mdb-engine 0.1.6__py3-none-any.whl → 0.1.7__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 (75) hide show
  1. mdb_engine/__init__.py +38 -6
  2. mdb_engine/auth/README.md +534 -11
  3. mdb_engine/auth/__init__.py +129 -28
  4. mdb_engine/auth/audit.py +592 -0
  5. mdb_engine/auth/casbin_factory.py +10 -14
  6. mdb_engine/auth/config_helpers.py +7 -6
  7. mdb_engine/auth/cookie_utils.py +3 -7
  8. mdb_engine/auth/csrf.py +373 -0
  9. mdb_engine/auth/decorators.py +3 -10
  10. mdb_engine/auth/dependencies.py +37 -45
  11. mdb_engine/auth/helpers.py +3 -3
  12. mdb_engine/auth/integration.py +30 -73
  13. mdb_engine/auth/jwt.py +2 -6
  14. mdb_engine/auth/middleware.py +77 -34
  15. mdb_engine/auth/oso_factory.py +16 -36
  16. mdb_engine/auth/provider.py +17 -38
  17. mdb_engine/auth/rate_limiter.py +504 -0
  18. mdb_engine/auth/restrictions.py +8 -24
  19. mdb_engine/auth/session_manager.py +14 -29
  20. mdb_engine/auth/shared_middleware.py +600 -0
  21. mdb_engine/auth/shared_users.py +759 -0
  22. mdb_engine/auth/token_store.py +14 -28
  23. mdb_engine/auth/users.py +54 -113
  24. mdb_engine/auth/utils.py +213 -15
  25. mdb_engine/cli/commands/generate.py +545 -9
  26. mdb_engine/cli/commands/validate.py +3 -7
  27. mdb_engine/cli/utils.py +3 -3
  28. mdb_engine/config.py +7 -21
  29. mdb_engine/constants.py +65 -0
  30. mdb_engine/core/README.md +117 -6
  31. mdb_engine/core/__init__.py +39 -7
  32. mdb_engine/core/app_registration.py +22 -41
  33. mdb_engine/core/app_secrets.py +290 -0
  34. mdb_engine/core/connection.py +18 -9
  35. mdb_engine/core/encryption.py +223 -0
  36. mdb_engine/core/engine.py +758 -95
  37. mdb_engine/core/index_management.py +12 -16
  38. mdb_engine/core/manifest.py +424 -135
  39. mdb_engine/core/ray_integration.py +435 -0
  40. mdb_engine/core/seeding.py +10 -18
  41. mdb_engine/core/service_initialization.py +12 -23
  42. mdb_engine/core/types.py +2 -5
  43. mdb_engine/database/README.md +112 -16
  44. mdb_engine/database/__init__.py +17 -6
  45. mdb_engine/database/abstraction.py +25 -37
  46. mdb_engine/database/connection.py +11 -18
  47. mdb_engine/database/query_validator.py +367 -0
  48. mdb_engine/database/resource_limiter.py +204 -0
  49. mdb_engine/database/scoped_wrapper.py +713 -196
  50. mdb_engine/embeddings/__init__.py +17 -9
  51. mdb_engine/embeddings/dependencies.py +1 -3
  52. mdb_engine/embeddings/service.py +11 -25
  53. mdb_engine/exceptions.py +92 -0
  54. mdb_engine/indexes/README.md +30 -13
  55. mdb_engine/indexes/__init__.py +1 -0
  56. mdb_engine/indexes/helpers.py +1 -1
  57. mdb_engine/indexes/manager.py +50 -114
  58. mdb_engine/memory/README.md +2 -2
  59. mdb_engine/memory/__init__.py +1 -2
  60. mdb_engine/memory/service.py +30 -87
  61. mdb_engine/observability/README.md +4 -2
  62. mdb_engine/observability/__init__.py +26 -9
  63. mdb_engine/observability/health.py +8 -9
  64. mdb_engine/observability/metrics.py +32 -12
  65. mdb_engine/routing/README.md +1 -1
  66. mdb_engine/routing/__init__.py +1 -3
  67. mdb_engine/routing/websockets.py +25 -60
  68. mdb_engine-0.1.7.dist-info/METADATA +285 -0
  69. mdb_engine-0.1.7.dist-info/RECORD +85 -0
  70. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  71. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  72. {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/WHEEL +0 -0
  73. {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/entry_points.txt +0 -0
  74. {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/licenses/LICENSE +0 -0
  75. {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/top_level.txt +0 -0
@@ -12,9 +12,12 @@ from typing import Any, Dict, Optional
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
 
@@ -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
@@ -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:
@@ -324,15 +314,13 @@ async def _setup_token_management(
324
314
  """Initialize token management (blacklist and session manager)."""
325
315
  if token_management.get("auto_setup", True):
326
316
  try:
327
- db = engine.get_database()
317
+ db = engine.get_scoped_db(slug_id)
328
318
  await initialize_token_management(app, db)
329
319
 
330
320
  # Configure session fingerprinting if session manager exists
331
321
  session_mgr = getattr(app.state, "session_manager", None)
332
322
  if session_mgr:
333
- fingerprinting_config = app.state.security_config.get(
334
- "session_fingerprinting", {}
335
- )
323
+ fingerprinting_config = app.state.security_config.get("session_fingerprinting", {})
336
324
  session_mgr.configure_fingerprinting(
337
325
  enabled=fingerprinting_config.get("enabled", True),
338
326
  strict=fingerprinting_config.get("strict_mode", False),
@@ -355,9 +343,7 @@ async def _setup_security_middleware(
355
343
  app: FastAPI, slug_id: str, security_config: Dict[str, Any]
356
344
  ) -> None:
357
345
  """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
- ):
346
+ if security_config.get("csrf_protection", True) or security_config.get("require_https", False):
361
347
  try:
362
348
  from .middleware import SecurityMiddleware
363
349
 
@@ -384,9 +370,7 @@ async def _setup_security_middleware(
384
370
  f"This is normal when using lifespan context managers."
385
371
  )
386
372
  else:
387
- logger.warning(
388
- f"Could not set up security middleware for {slug_id}: {e}"
389
- )
373
+ logger.warning(f"Could not set up security middleware for {slug_id}: {e}")
390
374
  except (AttributeError, TypeError, ValueError, RuntimeError, ImportError) as e:
391
375
  logger.warning(f"Could not set up security middleware for {slug_id}: {e}")
392
376
 
@@ -409,9 +393,7 @@ async def _setup_cors_and_observability(
409
393
  app.state.cors_config = merge_config_with_defaults(cors_config, CORS_DEFAULTS)
410
394
 
411
395
  # Extract and store observability config
412
- observability_config = (
413
- manifest_data.get("observability", {}) if manifest_data else {}
414
- )
396
+ observability_config = manifest_data.get("observability", {}) if manifest_data else {}
415
397
  app.state.observability_config = merge_config_with_defaults(
416
398
  observability_config, OBSERVABILITY_DEFAULTS
417
399
  )
@@ -421,9 +403,7 @@ async def _setup_cors_and_observability(
421
403
  try:
422
404
  # Check if CORS middleware already exists to avoid duplication
423
405
  if _has_cors_middleware(app):
424
- logger.debug(
425
- f"CORS middleware already exists for {slug_id}, skipping addition"
426
- )
406
+ logger.debug(f"CORS middleware already exists for {slug_id}, skipping addition")
427
407
  else:
428
408
  from fastapi.middleware.cors import CORSMiddleware
429
409
 
@@ -431,44 +411,29 @@ async def _setup_cors_and_observability(
431
411
  try:
432
412
  app.add_middleware(
433
413
  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
- ),
414
+ allow_origins=app.state.cors_config.get("allow_origins", ["*"]),
415
+ allow_credentials=app.state.cors_config.get("allow_credentials", False),
440
416
  allow_methods=app.state.cors_config.get(
441
417
  "allow_methods",
442
418
  ["GET", "POST", "PUT", "DELETE", "PATCH"],
443
419
  ),
444
- allow_headers=app.state.cors_config.get(
445
- "allow_headers", ["*"]
446
- ),
447
- expose_headers=app.state.cors_config.get(
448
- "expose_headers", []
449
- ),
420
+ allow_headers=app.state.cors_config.get("allow_headers", ["*"]),
421
+ expose_headers=app.state.cors_config.get("expose_headers", []),
450
422
  max_age=app.state.cors_config.get("max_age", 3600),
451
423
  )
452
424
  logger.info(f"CORS middleware added for {slug_id}")
453
425
  except (RuntimeError, ValueError) as e:
454
426
  error_msg = str(e).lower()
455
- if (
456
- "cannot add middleware" in error_msg
457
- or "middleware" in error_msg
458
- ):
427
+ if "cannot add middleware" in error_msg or "middleware" in error_msg:
459
428
  logger.debug(
460
429
  f"CORS middleware not added for {slug_id} - "
461
430
  f"app middleware stack already initialized. "
462
431
  f"This is normal when using lifespan context managers."
463
432
  )
464
433
  else:
465
- logger.warning(
466
- f"Could not set up CORS middleware for {slug_id}: {e}"
467
- )
434
+ logger.warning(f"Could not set up CORS middleware for {slug_id}: {e}")
468
435
  else:
469
- logger.warning(
470
- f"CORS middleware not added for {slug_id} - app already started"
471
- )
436
+ logger.warning(f"CORS middleware not added for {slug_id} - app already started")
472
437
  except (AttributeError, TypeError, ValueError, RuntimeError, ImportError) as e:
473
438
  logger.warning(f"Could not set up CORS middleware for {slug_id}: {e}")
474
439
 
@@ -480,9 +445,7 @@ async def _setup_cors_and_observability(
480
445
  from .middleware import StaleSessionMiddleware
481
446
 
482
447
  try:
483
- app.add_middleware(
484
- StaleSessionMiddleware, slug_id=slug_id, engine=engine
485
- )
448
+ app.add_middleware(StaleSessionMiddleware, slug_id=slug_id, engine=engine)
486
449
  logger.info(f"Stale session cleanup middleware added for {slug_id}")
487
450
  except (RuntimeError, ValueError) as e:
488
451
  error_msg = str(e).lower()
@@ -493,13 +456,9 @@ async def _setup_cors_and_observability(
493
456
  f"This is normal when using lifespan context managers."
494
457
  )
495
458
  else:
496
- logger.warning(
497
- f"Could not set up stale session middleware for {slug_id}: {e}"
498
- )
459
+ logger.warning(f"Could not set up stale session middleware for {slug_id}: {e}")
499
460
  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
- )
461
+ logger.warning(f"Could not set up stale session middleware for {slug_id}: {e}")
503
462
 
504
463
 
505
464
  async def setup_auth_from_manifest(app: FastAPI, engine, slug_id: str) -> bool:
@@ -572,7 +531,5 @@ async def setup_auth_from_manifest(app: FastAPI, engine, slug_id: str) -> bool:
572
531
  KeyError,
573
532
  ConnectionError,
574
533
  ) as e:
575
- logger.error(
576
- f"Error setting up auth from manifest for {slug_id}: {e}", exc_info=True
577
- )
534
+ logger.error(f"Error setting up auth from manifest for {slug_id}: {e}", exc_info=True)
578
535
  return False
mdb_engine/auth/jwt.py CHANGED
@@ -164,9 +164,7 @@ def generate_token_pair(
164
164
  "jti": access_jti,
165
165
  "device_id": device_id,
166
166
  }
167
- access_token = encode_jwt_token(
168
- access_payload, secret_key, expires_in=access_token_ttl
169
- )
167
+ access_token = encode_jwt_token(access_payload, secret_key, expires_in=access_token_ttl)
170
168
 
171
169
  # Generate refresh token
172
170
  refresh_jti = str(uuid.uuid4())
@@ -177,9 +175,7 @@ def generate_token_pair(
177
175
  "email": user_data.get("email"),
178
176
  "device_id": device_id,
179
177
  }
180
- refresh_token = encode_jwt_token(
181
- refresh_payload, secret_key, expires_in=refresh_token_ttl
182
- )
178
+ refresh_token = encode_jwt_token(refresh_payload, secret_key, expires_in=refresh_token_ttl)
183
179
 
184
180
  # Token metadata
185
181
  token_metadata = {
@@ -4,12 +4,18 @@ Security Middleware
4
4
  Middleware for enforcing security settings from manifest configuration.
5
5
 
6
6
  This module is part of MDB_ENGINE - MongoDB Engine.
7
+
8
+ Security Features:
9
+ - HTTPS enforcement in production
10
+ - HSTS (HTTP Strict Transport Security) header
11
+ - Security headers (X-Content-Type-Options, X-Frame-Options, etc.)
12
+ - CSRF token generation (legacy, prefer CSRFMiddleware for new apps)
7
13
  """
8
14
 
9
15
  import logging
10
16
  import os
11
17
  import secrets
12
- from typing import Awaitable, Callable
18
+ from typing import Any, Awaitable, Callable, Dict, Optional
13
19
 
14
20
  from fastapi import HTTPException, Request, Response, status
15
21
  from fastapi.responses import RedirectResponse
@@ -17,6 +23,18 @@ from starlette.middleware.base import BaseHTTPMiddleware
17
23
 
18
24
  logger = logging.getLogger(__name__)
19
25
 
26
+ # Default HSTS settings
27
+ DEFAULT_HSTS_MAX_AGE = 31536000 # 1 year in seconds
28
+
29
+
30
+ def _is_production() -> bool:
31
+ """Check if we're running in production environment."""
32
+ return (
33
+ os.getenv("MDB_ENGINE_ENV", "").lower() == "production"
34
+ or os.getenv("ENVIRONMENT", "").lower() == "production"
35
+ or os.getenv("G_NOME_ENV", "").lower() == "production"
36
+ )
37
+
20
38
 
21
39
  class SecurityMiddleware(BaseHTTPMiddleware):
22
40
  """
@@ -24,9 +42,9 @@ class SecurityMiddleware(BaseHTTPMiddleware):
24
42
 
25
43
  Features:
26
44
  - HTTPS enforcement in production
27
- - CSRF token generation and validation
28
- - Security headers
29
- - Token validation
45
+ - HSTS header for forcing HTTPS
46
+ - Security headers (X-Content-Type-Options, X-Frame-Options, etc.)
47
+ - Legacy CSRF token generation (prefer CSRFMiddleware for new apps)
30
48
  """
31
49
 
32
50
  def __init__(
@@ -35,6 +53,7 @@ class SecurityMiddleware(BaseHTTPMiddleware):
35
53
  require_https: bool = False,
36
54
  csrf_protection: bool = True,
37
55
  security_headers: bool = True,
56
+ hsts_config: Optional[Dict[str, Any]] = None,
38
57
  ):
39
58
  """
40
59
  Initialize security middleware.
@@ -42,38 +61,60 @@ class SecurityMiddleware(BaseHTTPMiddleware):
42
61
  Args:
43
62
  app: FastAPI application
44
63
  require_https: Require HTTPS in production (default: False, auto-detected)
45
- csrf_protection: Enable CSRF protection (default: True)
64
+ csrf_protection: Enable legacy CSRF protection (default: True)
46
65
  security_headers: Add security headers (default: True)
66
+ hsts_config: HSTS configuration dict with keys:
67
+ - enabled: Enable HSTS (default: True in production)
68
+ - max_age: Max-age in seconds (default: 31536000)
69
+ - include_subdomains: Include subdomains (default: True)
70
+ - preload: Add preload directive (default: False)
47
71
  """
48
72
  super().__init__(app)
49
73
  self.require_https = require_https
50
74
  self.csrf_protection = csrf_protection
51
75
  self.security_headers = security_headers
52
76
 
77
+ # HSTS configuration
78
+ self.hsts_config = hsts_config or {}
79
+ self.hsts_enabled = self.hsts_config.get("enabled", True)
80
+ self.hsts_max_age = self.hsts_config.get("max_age", DEFAULT_HSTS_MAX_AGE)
81
+ self.hsts_include_subdomains = self.hsts_config.get("include_subdomains", True)
82
+ self.hsts_preload = self.hsts_config.get("preload", False)
83
+
84
+ def _build_hsts_header(self) -> str:
85
+ """Build the HSTS header value."""
86
+ parts = [f"max-age={self.hsts_max_age}"]
87
+
88
+ if self.hsts_include_subdomains:
89
+ parts.append("includeSubDomains")
90
+
91
+ if self.hsts_preload:
92
+ parts.append("preload")
93
+
94
+ return "; ".join(parts)
95
+
53
96
  async def dispatch(
54
97
  self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
55
98
  ) -> Response:
56
99
  """
57
100
  Process request through security middleware.
58
101
  """
102
+ is_production = _is_production()
103
+ is_https = request.url.scheme == "https"
104
+
59
105
  # Check HTTPS requirement
60
- if self.require_https:
61
- is_production = (
62
- os.getenv("G_NOME_ENV") == "production"
63
- or os.getenv("ENVIRONMENT") == "production"
64
- )
65
- if is_production and request.url.scheme != "https":
66
- if request.method == "GET":
67
- # Redirect to HTTPS
68
- https_url = str(request.url).replace("http://", "https://", 1)
69
- return RedirectResponse(url=https_url, status_code=301)
70
- else:
71
- raise HTTPException(
72
- status_code=status.HTTP_403_FORBIDDEN,
73
- detail="HTTPS required in production",
74
- )
75
-
76
- # Generate CSRF token if not present (for GET requests)
106
+ if self.require_https and is_production and not is_https:
107
+ if request.method == "GET":
108
+ # Redirect to HTTPS
109
+ https_url = str(request.url).replace("http://", "https://", 1)
110
+ return RedirectResponse(url=https_url, status_code=301)
111
+ else:
112
+ raise HTTPException(
113
+ status_code=status.HTTP_403_FORBIDDEN,
114
+ detail="HTTPS required in production",
115
+ )
116
+
117
+ # Generate CSRF token if not present (for GET requests) - legacy support
77
118
  if self.csrf_protection and request.method == "GET":
78
119
  csrf_token = request.cookies.get("csrf_token")
79
120
  if not csrf_token:
@@ -90,19 +131,27 @@ class SecurityMiddleware(BaseHTTPMiddleware):
90
131
  response.headers["X-XSS-Protection"] = "1; mode=block"
91
132
  response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
92
133
 
134
+ # Permissions-Policy (modern replacement for some legacy headers)
135
+ response.headers["Permissions-Policy"] = (
136
+ "accelerometer=(), camera=(), geolocation=(), gyroscope=(), "
137
+ "magnetometer=(), microphone=(), payment=(), usb=()"
138
+ )
139
+
93
140
  # Content Security Policy (basic)
94
141
  if request.url.path.startswith("/api"):
95
142
  response.headers["Content-Security-Policy"] = "default-src 'self'"
96
143
 
97
- # Set CSRF token cookie if generated
144
+ # Add HSTS header in production (only over HTTPS or always if configured)
145
+ if self.hsts_enabled and (is_production or is_https):
146
+ response.headers["Strict-Transport-Security"] = self._build_hsts_header()
147
+
148
+ # Set CSRF token cookie if generated (legacy support)
98
149
  if (
99
150
  self.csrf_protection
100
151
  and request.method == "GET"
101
152
  and not request.cookies.get("csrf_token")
102
153
  ):
103
154
  csrf_token = secrets.token_urlsafe(32)
104
- is_https = request.url.scheme == "https"
105
- is_production = os.getenv("G_NOME_ENV") == "production"
106
155
  response.set_cookie(
107
156
  key="csrf_token",
108
157
  value=csrf_token,
@@ -154,10 +203,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
154
203
  # Check if we need to clear a stale session cookie
155
204
  # Only act if explicitly flagged - this ensures we don't interfere with
156
205
  # apps that don't use get_app_user()
157
- if (
158
- hasattr(request.state, "clear_stale_session")
159
- and request.state.clear_stale_session
160
- ):
206
+ if hasattr(request.state, "clear_stale_session") and request.state.clear_stale_session:
161
207
  try:
162
208
  # Get cookie name from app config
163
209
  cookie_name = None
@@ -197,8 +243,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
197
243
 
198
244
  # Get cookie settings to match how it was set
199
245
  should_use_secure = (
200
- request.url.scheme == "https"
201
- or os.getenv("G_NOME_ENV") == "production"
246
+ request.url.scheme == "https" or os.getenv("G_NOME_ENV") == "production"
202
247
  )
203
248
 
204
249
  # Delete the stale cookie
@@ -208,9 +253,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
208
253
  secure=should_use_secure,
209
254
  samesite="lax",
210
255
  )
211
- logger.debug(
212
- f"Cleared stale session cookie '{cookie_name}' for {self.slug_id}"
213
- )
256
+ logger.debug(f"Cleared stale session cookie '{cookie_name}' for {self.slug_id}")
214
257
  except (ValueError, TypeError, AttributeError, RuntimeError) as e:
215
258
  # Don't fail the request if cookie cleanup fails
216
259
  logger.warning(
@@ -11,7 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  import logging
13
13
  import os
14
- from typing import TYPE_CHECKING, Any, Dict, List, Optional
14
+ from typing import TYPE_CHECKING, Any, Optional
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from .provider import OsoAdapter
@@ -49,10 +49,8 @@ async def create_oso_cloud_client(
49
49
  from oso_cloud import Oso
50
50
 
51
51
  logger.debug("✅ Imported Oso from oso_cloud")
52
- except ImportError:
53
- raise ImportError(
54
- "OSO Cloud SDK not installed. Install with: pip install oso-cloud"
55
- )
52
+ except ImportError as e:
53
+ raise ImportError("OSO Cloud SDK not installed. Install with: pip install oso-cloud") from e
56
54
 
57
55
  # Get API key from parameter or environment
58
56
  if not api_key:
@@ -81,9 +79,7 @@ async def create_oso_cloud_client(
81
79
 
82
80
  # Note: OSO client creation doesn't actually connect to the server
83
81
  # The connection happens on first API call, so we'll catch errors then
84
- logger.info(
85
- f"✅ OSO Cloud client created successfully (URL: {url or 'default'})"
86
- )
82
+ logger.info(f"✅ OSO Cloud client created successfully (URL: {url or 'default'})")
87
83
  if url:
88
84
  logger.info(f" Using OSO Dev Server at: {url}")
89
85
  return oso_client
@@ -118,8 +114,8 @@ async def create_oso_cloud_client(
118
114
 
119
115
  async def setup_initial_oso_facts(
120
116
  authz_provider: OsoAdapter,
121
- initial_roles: Optional[List[Dict[str, Any]]] = None,
122
- initial_policies: Optional[List[Dict[str, Any]]] = None,
117
+ initial_roles: Optional[list[dict[str, Any]]] = None,
118
+ initial_policies: Optional[list[dict[str, Any]]] = None,
123
119
  ) -> None:
124
120
  """
125
121
  Set up initial roles and policies in OSO Cloud.
@@ -137,28 +133,22 @@ async def setup_initial_oso_facts(
137
133
  try:
138
134
  user = role_assignment.get("user")
139
135
  role = role_assignment.get("role")
140
- resource = role_assignment.get(
141
- "resource", "documents"
142
- ) # Default to "documents"
136
+ resource = role_assignment.get("resource", "documents") # Default to "documents"
143
137
 
144
138
  if user and role:
145
139
  # For OSO Cloud, we add has_role facts with resource context
146
140
  # This supports resource-based authorization
147
141
  await authz_provider.add_role_for_user(user, role, resource)
148
- logger.debug(
149
- f"Added role '{role}' for user '{user}' on resource '{resource}'"
150
- )
142
+ logger.debug(f"Added role '{role}' for user '{user}' on resource '{resource}'")
151
143
  except (ValueError, TypeError, AttributeError, RuntimeError) as e:
152
- logger.warning(
153
- f"Failed to add initial role assignment {role_assignment}: {e}"
154
- )
144
+ logger.warning(f"Failed to add initial role assignment {role_assignment}: {e}")
155
145
 
156
146
  # Note: initial_policies are not used - we use has_role facts instead
157
147
  # The policy derives permissions from roles, not from explicit grants_permission facts
158
148
 
159
149
 
160
150
  async def initialize_oso_from_manifest(
161
- engine, app_slug: str, auth_config: Dict[str, Any]
151
+ engine, app_slug: str, auth_config: dict[str, Any]
162
152
  ) -> Optional[OsoAdapter]:
163
153
  """
164
154
  Initialize OSO Cloud provider from manifest configuration.
@@ -181,9 +171,7 @@ async def initialize_oso_from_manifest(
181
171
 
182
172
  # Only proceed if provider is oso
183
173
  if provider != "oso":
184
- logger.debug(
185
- f"Provider is '{provider}', not 'oso' - skipping OSO initialization"
186
- )
174
+ logger.debug(f"Provider is '{provider}', not 'oso' - skipping OSO initialization")
187
175
  return None
188
176
 
189
177
  logger.info(f"Initializing OSO Cloud provider for app '{app_slug}'...")
@@ -230,18 +218,12 @@ async def initialize_oso_from_manifest(
230
218
  test_actor = Value("User", "test")
231
219
  test_resource = Value("Document", "test")
232
220
  # This might fail, but it tests if the server is responding
233
- await asyncio.to_thread(
234
- oso_client.authorize, test_actor, "read", test_resource
235
- )
221
+ await asyncio.to_thread(oso_client.authorize, test_actor, "read", test_resource)
236
222
  logger.debug("✅ OSO Dev Server connection test successful")
237
223
  except (TimeoutError, OSError, RuntimeError) as test_error:
238
224
  # Type 2: Recoverable - connection test failed, check if it's a connection error
239
225
  error_str = str(test_error).lower()
240
- if (
241
- "connection" in error_str
242
- or "refused" in error_str
243
- or "timeout" in error_str
244
- ):
226
+ if "connection" in error_str or "refused" in error_str or "timeout" in error_str:
245
227
  logger.warning(
246
228
  f"⚠️ OSO Dev Server connection test failed - "
247
229
  f"server might not be ready: {test_error}"
@@ -289,9 +271,7 @@ async def initialize_oso_from_manifest(
289
271
  )
290
272
  logger.info("✅ Initial OSO facts set up successfully")
291
273
  except (ValueError, TypeError, AttributeError, RuntimeError) as e:
292
- logger.warning(
293
- f"⚠️ Failed to set up initial OSO facts: {e}", exc_info=True
294
- )
274
+ logger.warning(f"⚠️ Failed to set up initial OSO facts: {e}", exc_info=True)
295
275
  # Continue anyway - adapter is still usable
296
276
 
297
277
  logger.info(f"✅ OSO Cloud provider initialized for app '{app_slug}'")
@@ -299,13 +279,13 @@ async def initialize_oso_from_manifest(
299
279
  return adapter
300
280
 
301
281
  except ImportError as e:
302
- logger.error(
282
+ logger.exception(
303
283
  f"❌ OSO Cloud SDK not available for app '{app_slug}': {e}. "
304
284
  "Install with: pip install oso-cloud"
305
285
  )
306
286
  return None
307
287
  except ValueError as e:
308
- logger.error(f"❌ OSO Cloud configuration error for app '{app_slug}': {e}")
288
+ logger.exception(f"❌ OSO Cloud configuration error for app '{app_slug}': {e}")
309
289
  return None
310
290
  except (
311
291
  ImportError,