mdb-engine 0.1.6__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/README.md +144 -0
  2. mdb_engine/__init__.py +37 -0
  3. mdb_engine/auth/README.md +631 -0
  4. mdb_engine/auth/__init__.py +128 -0
  5. mdb_engine/auth/casbin_factory.py +199 -0
  6. mdb_engine/auth/casbin_models.py +46 -0
  7. mdb_engine/auth/config_defaults.py +71 -0
  8. mdb_engine/auth/config_helpers.py +213 -0
  9. mdb_engine/auth/cookie_utils.py +158 -0
  10. mdb_engine/auth/decorators.py +350 -0
  11. mdb_engine/auth/dependencies.py +747 -0
  12. mdb_engine/auth/helpers.py +64 -0
  13. mdb_engine/auth/integration.py +578 -0
  14. mdb_engine/auth/jwt.py +225 -0
  15. mdb_engine/auth/middleware.py +241 -0
  16. mdb_engine/auth/oso_factory.py +323 -0
  17. mdb_engine/auth/provider.py +570 -0
  18. mdb_engine/auth/restrictions.py +271 -0
  19. mdb_engine/auth/session_manager.py +477 -0
  20. mdb_engine/auth/token_lifecycle.py +213 -0
  21. mdb_engine/auth/token_store.py +289 -0
  22. mdb_engine/auth/users.py +1516 -0
  23. mdb_engine/auth/utils.py +614 -0
  24. mdb_engine/cli/__init__.py +13 -0
  25. mdb_engine/cli/commands/__init__.py +7 -0
  26. mdb_engine/cli/commands/generate.py +105 -0
  27. mdb_engine/cli/commands/migrate.py +83 -0
  28. mdb_engine/cli/commands/show.py +70 -0
  29. mdb_engine/cli/commands/validate.py +63 -0
  30. mdb_engine/cli/main.py +41 -0
  31. mdb_engine/cli/utils.py +92 -0
  32. mdb_engine/config.py +217 -0
  33. mdb_engine/constants.py +160 -0
  34. mdb_engine/core/README.md +542 -0
  35. mdb_engine/core/__init__.py +42 -0
  36. mdb_engine/core/app_registration.py +392 -0
  37. mdb_engine/core/connection.py +243 -0
  38. mdb_engine/core/engine.py +749 -0
  39. mdb_engine/core/index_management.py +162 -0
  40. mdb_engine/core/manifest.py +2793 -0
  41. mdb_engine/core/seeding.py +179 -0
  42. mdb_engine/core/service_initialization.py +355 -0
  43. mdb_engine/core/types.py +413 -0
  44. mdb_engine/database/README.md +522 -0
  45. mdb_engine/database/__init__.py +31 -0
  46. mdb_engine/database/abstraction.py +635 -0
  47. mdb_engine/database/connection.py +387 -0
  48. mdb_engine/database/scoped_wrapper.py +1721 -0
  49. mdb_engine/embeddings/README.md +184 -0
  50. mdb_engine/embeddings/__init__.py +62 -0
  51. mdb_engine/embeddings/dependencies.py +193 -0
  52. mdb_engine/embeddings/service.py +759 -0
  53. mdb_engine/exceptions.py +167 -0
  54. mdb_engine/indexes/README.md +651 -0
  55. mdb_engine/indexes/__init__.py +21 -0
  56. mdb_engine/indexes/helpers.py +145 -0
  57. mdb_engine/indexes/manager.py +895 -0
  58. mdb_engine/memory/README.md +451 -0
  59. mdb_engine/memory/__init__.py +30 -0
  60. mdb_engine/memory/service.py +1285 -0
  61. mdb_engine/observability/README.md +515 -0
  62. mdb_engine/observability/__init__.py +42 -0
  63. mdb_engine/observability/health.py +296 -0
  64. mdb_engine/observability/logging.py +161 -0
  65. mdb_engine/observability/metrics.py +297 -0
  66. mdb_engine/routing/README.md +462 -0
  67. mdb_engine/routing/__init__.py +73 -0
  68. mdb_engine/routing/websockets.py +813 -0
  69. mdb_engine/utils/__init__.py +7 -0
  70. mdb_engine-0.1.6.dist-info/METADATA +213 -0
  71. mdb_engine-0.1.6.dist-info/RECORD +75 -0
  72. mdb_engine-0.1.6.dist-info/WHEEL +5 -0
  73. mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
  74. mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
  75. mdb_engine-0.1.6.dist-info/top_level.txt +1 -0
mdb_engine/auth/jwt.py ADDED
@@ -0,0 +1,225 @@
1
+ """
2
+ JWT Token Utilities
3
+
4
+ Provides JWT encoding and decoding utilities with automatic format handling.
5
+ Supports access tokens, refresh tokens, and token pairs.
6
+
7
+ This module is part of MDB_ENGINE - MongoDB Engine.
8
+ """
9
+
10
+ import logging
11
+ import uuid
12
+ from datetime import datetime, timedelta
13
+ from typing import Any, Dict, Optional, Tuple
14
+
15
+ import jwt
16
+
17
+ from ..config import ACCESS_TOKEN_TTL as CONFIG_ACCESS_TTL
18
+ from ..config import REFRESH_TOKEN_TTL as CONFIG_REFRESH_TTL
19
+ from ..constants import CURRENT_TOKEN_VERSION
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def decode_jwt_token(token: Any, secret_key: str) -> Dict[str, Any]:
25
+ """
26
+ Helper function to decode JWT tokens with automatic fallback to bytes format.
27
+
28
+ Handles cases where PyJWT might expect bytes instead of strings (version-specific behavior).
29
+
30
+ Args:
31
+ token: JWT token (can be str, bytes, or other)
32
+ secret_key: Secret key for decoding (str)
33
+
34
+ Returns:
35
+ Decoded JWT payload as dict
36
+
37
+ Raises:
38
+ jwt.ExpiredSignatureError: If token has expired
39
+ jwt.InvalidTokenError: If token is invalid
40
+ """
41
+ # Normalize token to string first
42
+ if isinstance(token, bytes):
43
+ token_str = token.decode("utf-8")
44
+ elif isinstance(token, str):
45
+ token_str = token
46
+ else:
47
+ token_str = str(token)
48
+
49
+ # Normalize secret_key to string
50
+ if isinstance(secret_key, bytes):
51
+ secret_key_str = secret_key.decode("utf-8")
52
+ elif isinstance(secret_key, str):
53
+ secret_key_str = secret_key
54
+ else:
55
+ secret_key_str = str(secret_key)
56
+
57
+ # Try decoding with string format first (standard PyJWT behavior)
58
+ try:
59
+ return jwt.decode(token_str, secret_key_str, algorithms=["HS256"])
60
+ except jwt.InvalidTokenError as e:
61
+ # If string format fails with "must be bytes" error, try bytes format
62
+ error_msg = str(e)
63
+ if "must be a <class 'bytes'>" in error_msg or (
64
+ "bytes" in error_msg.lower() and "token" in error_msg.lower()
65
+ ):
66
+ logger.debug(f"JWT decode: Retrying with bytes format (error: {e})")
67
+ # Convert to bytes and try again
68
+ token_bytes = token_str.encode("utf-8")
69
+ secret_key_bytes = secret_key_str.encode("utf-8")
70
+ return jwt.decode(token_bytes, secret_key_bytes, algorithms=["HS256"])
71
+ else:
72
+ # Re-raise if it's a different error
73
+ raise
74
+
75
+
76
+ def encode_jwt_token(
77
+ payload: Dict[str, Any], secret_key: str, expires_in: Optional[int] = None
78
+ ) -> str:
79
+ """
80
+ Encode a JWT token with enhanced claims.
81
+
82
+ Args:
83
+ payload: Token payload (will be enhanced with standard claims)
84
+ secret_key: Secret key for signing
85
+ expires_in: Optional expiration time in seconds (defaults to ACCESS_TOKEN_TTL)
86
+
87
+ Returns:
88
+ Encoded JWT token string
89
+ """
90
+ # Normalize secret_key
91
+ if isinstance(secret_key, bytes):
92
+ secret_key_str = secret_key.decode("utf-8")
93
+ elif isinstance(secret_key, str):
94
+ secret_key_str = secret_key
95
+ else:
96
+ secret_key_str = str(secret_key)
97
+
98
+ # Create enhanced payload with standard claims
99
+ now = datetime.utcnow()
100
+ enhanced_payload = {
101
+ **payload,
102
+ "iat": now, # Issued at
103
+ "nbf": now, # Not before
104
+ "jti": payload.get("jti") or str(uuid.uuid4()), # JWT ID
105
+ "version": payload.get("version", CURRENT_TOKEN_VERSION), # Token version
106
+ }
107
+
108
+ # Add expiration
109
+ if expires_in is None:
110
+ expires_in = CONFIG_ACCESS_TTL
111
+ enhanced_payload["exp"] = now + timedelta(seconds=expires_in)
112
+
113
+ # Encode token
114
+ token = jwt.encode(enhanced_payload, secret_key_str, algorithm="HS256")
115
+
116
+ # Ensure token is a string (some PyJWT versions return bytes)
117
+ if isinstance(token, bytes):
118
+ token = token.decode("utf-8")
119
+ elif not isinstance(token, str):
120
+ token = str(token)
121
+
122
+ return token
123
+
124
+
125
+ def generate_token_pair(
126
+ user_data: Dict[str, Any],
127
+ secret_key: str,
128
+ device_info: Optional[Dict[str, Any]] = None,
129
+ access_token_ttl: Optional[int] = None,
130
+ refresh_token_ttl: Optional[int] = None,
131
+ ) -> Tuple[str, str, Dict[str, Any]]:
132
+ """
133
+ Generate a pair of access and refresh tokens.
134
+
135
+ Args:
136
+ user_data: User information to include in tokens (email, user_id, etc.)
137
+ secret_key: Secret key for signing tokens
138
+ device_info: Optional device information (device_id, user_agent, ip_address)
139
+ access_token_ttl: Optional access token TTL in seconds (defaults to config)
140
+ refresh_token_ttl: Optional refresh token TTL in seconds (defaults to config)
141
+
142
+ Returns:
143
+ Tuple of (access_token, refresh_token, token_metadata)
144
+ token_metadata contains: jti (access), refresh_jti, expires_at, device_id
145
+ """
146
+ if access_token_ttl is None:
147
+ access_token_ttl = CONFIG_ACCESS_TTL
148
+ if refresh_token_ttl is None:
149
+ refresh_token_ttl = CONFIG_REFRESH_TTL
150
+
151
+ device_id = None
152
+ if device_info:
153
+ device_id = device_info.get("device_id") or str(uuid.uuid4())
154
+ else:
155
+ device_id = str(uuid.uuid4())
156
+
157
+ now = datetime.utcnow()
158
+
159
+ # Generate access token
160
+ access_jti = str(uuid.uuid4())
161
+ access_payload = {
162
+ **user_data,
163
+ "type": "access",
164
+ "jti": access_jti,
165
+ "device_id": device_id,
166
+ }
167
+ access_token = encode_jwt_token(
168
+ access_payload, secret_key, expires_in=access_token_ttl
169
+ )
170
+
171
+ # Generate refresh token
172
+ refresh_jti = str(uuid.uuid4())
173
+ refresh_payload = {
174
+ "type": "refresh",
175
+ "jti": refresh_jti,
176
+ "user_id": user_data.get("user_id") or user_data.get("email"),
177
+ "email": user_data.get("email"),
178
+ "device_id": device_id,
179
+ }
180
+ refresh_token = encode_jwt_token(
181
+ refresh_payload, secret_key, expires_in=refresh_token_ttl
182
+ )
183
+
184
+ # Token metadata
185
+ token_metadata = {
186
+ "access_jti": access_jti,
187
+ "refresh_jti": refresh_jti,
188
+ "device_id": device_id,
189
+ "access_expires_at": now + timedelta(seconds=access_token_ttl),
190
+ "refresh_expires_at": now + timedelta(seconds=refresh_token_ttl),
191
+ "issued_at": now,
192
+ }
193
+
194
+ return access_token, refresh_token, token_metadata
195
+
196
+
197
+ def extract_token_metadata(token: str, secret_key: str) -> Optional[Dict[str, Any]]:
198
+ """
199
+ Extract metadata from a token without full validation.
200
+
201
+ Useful for getting token info before checking blacklist.
202
+
203
+ Args:
204
+ token: JWT token string
205
+ secret_key: Secret key for decoding
206
+
207
+ Returns:
208
+ Token metadata dict or None if invalid
209
+ """
210
+ try:
211
+ # Decode without verification to get claims
212
+ payload = jwt.decode(token, options={"verify_signature": False})
213
+ return {
214
+ "jti": payload.get("jti"),
215
+ "type": payload.get("type"),
216
+ "user_id": payload.get("user_id") or payload.get("email"),
217
+ "email": payload.get("email"),
218
+ "device_id": payload.get("device_id"),
219
+ "exp": payload.get("exp"),
220
+ "iat": payload.get("iat"),
221
+ "version": payload.get("version"),
222
+ }
223
+ except (ValueError, TypeError, AttributeError, KeyError) as e:
224
+ logger.debug(f"Error extracting token metadata: {e}")
225
+ return None
@@ -0,0 +1,241 @@
1
+ """
2
+ Security Middleware
3
+
4
+ Middleware for enforcing security settings from manifest configuration.
5
+
6
+ This module is part of MDB_ENGINE - MongoDB Engine.
7
+ """
8
+
9
+ import logging
10
+ import os
11
+ import secrets
12
+ from typing import Awaitable, Callable
13
+
14
+ from fastapi import HTTPException, Request, Response, status
15
+ from fastapi.responses import RedirectResponse
16
+ from starlette.middleware.base import BaseHTTPMiddleware
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class SecurityMiddleware(BaseHTTPMiddleware):
22
+ """
23
+ Middleware for enforcing security settings from manifest.
24
+
25
+ Features:
26
+ - HTTPS enforcement in production
27
+ - CSRF token generation and validation
28
+ - Security headers
29
+ - Token validation
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ app,
35
+ require_https: bool = False,
36
+ csrf_protection: bool = True,
37
+ security_headers: bool = True,
38
+ ):
39
+ """
40
+ Initialize security middleware.
41
+
42
+ Args:
43
+ app: FastAPI application
44
+ require_https: Require HTTPS in production (default: False, auto-detected)
45
+ csrf_protection: Enable CSRF protection (default: True)
46
+ security_headers: Add security headers (default: True)
47
+ """
48
+ super().__init__(app)
49
+ self.require_https = require_https
50
+ self.csrf_protection = csrf_protection
51
+ self.security_headers = security_headers
52
+
53
+ async def dispatch(
54
+ self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
55
+ ) -> Response:
56
+ """
57
+ Process request through security middleware.
58
+ """
59
+ # 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)
77
+ if self.csrf_protection and request.method == "GET":
78
+ csrf_token = request.cookies.get("csrf_token")
79
+ if not csrf_token:
80
+ csrf_token = secrets.token_urlsafe(32)
81
+ # Will be set in response
82
+
83
+ # Process request
84
+ response = await call_next(request)
85
+
86
+ # Set security headers
87
+ if self.security_headers:
88
+ response.headers["X-Content-Type-Options"] = "nosniff"
89
+ response.headers["X-Frame-Options"] = "DENY"
90
+ response.headers["X-XSS-Protection"] = "1; mode=block"
91
+ response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
92
+
93
+ # Content Security Policy (basic)
94
+ if request.url.path.startswith("/api"):
95
+ response.headers["Content-Security-Policy"] = "default-src 'self'"
96
+
97
+ # Set CSRF token cookie if generated
98
+ if (
99
+ self.csrf_protection
100
+ and request.method == "GET"
101
+ and not request.cookies.get("csrf_token")
102
+ ):
103
+ csrf_token = secrets.token_urlsafe(32)
104
+ is_https = request.url.scheme == "https"
105
+ is_production = os.getenv("G_NOME_ENV") == "production"
106
+ response.set_cookie(
107
+ key="csrf_token",
108
+ value=csrf_token,
109
+ httponly=True,
110
+ secure=is_https or is_production,
111
+ samesite="lax",
112
+ max_age=86400, # 24 hours
113
+ )
114
+
115
+ return response
116
+
117
+
118
+ class StaleSessionMiddleware(BaseHTTPMiddleware):
119
+ """
120
+ Middleware for cleaning up stale session cookies.
121
+
122
+ When get_app_user() detects a stale/invalid session cookie,
123
+ it sets request.state.clear_stale_session = True. This middleware
124
+ then removes the cookie from the response.
125
+ """
126
+
127
+ def __init__(self, app, slug_id: str, engine=None):
128
+ """
129
+ Initialize stale session middleware.
130
+
131
+ Args:
132
+ app: FastAPI application
133
+ slug_id: App slug identifier
134
+ engine: Optional MongoDBEngine instance for getting app config
135
+ """
136
+ super().__init__(app)
137
+ self.slug_id = slug_id
138
+ self.engine = engine
139
+
140
+ async def dispatch(
141
+ self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
142
+ ) -> Response:
143
+ """
144
+ Process request and clean up stale session cookies if needed.
145
+
146
+ This middleware only acts when request.state.clear_stale_session is set to True
147
+ by get_app_user() when it detects an invalid/stale session cookie.
148
+ It gracefully handles missing config and only processes requests for apps
149
+ that use auth.users.
150
+ """
151
+ # Process request first
152
+ response = await call_next(request)
153
+
154
+ # Check if we need to clear a stale session cookie
155
+ # Only act if explicitly flagged - this ensures we don't interfere with
156
+ # 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
+ ):
161
+ try:
162
+ # Get cookie name from app config
163
+ cookie_name = None
164
+
165
+ # Try to get from app state first (set during setup_auth_from_manifest)
166
+ if hasattr(request.app.state, "auth_config"):
167
+ try:
168
+ auth_config = request.app.state.auth_config
169
+ auth = auth_config.get("auth", {})
170
+ users_config = auth.get("users", {})
171
+ if users_config.get("enabled", False):
172
+ session_cookie_name = users_config.get(
173
+ "session_cookie_name", "app_session"
174
+ )
175
+ cookie_name = f"{session_cookie_name}_{self.slug_id}"
176
+ except (AttributeError, KeyError, TypeError):
177
+ pass
178
+
179
+ # Fallback: get from engine if available
180
+ if not cookie_name and self.engine:
181
+ try:
182
+ app_config = self.engine.get_app(self.slug_id)
183
+ if app_config:
184
+ auth = app_config.get("auth", {})
185
+ users_config = auth.get("users", {})
186
+ if users_config.get("enabled", False):
187
+ session_cookie_name = users_config.get(
188
+ "session_cookie_name", "app_session"
189
+ )
190
+ cookie_name = f"{session_cookie_name}_{self.slug_id}"
191
+ except (AttributeError, KeyError, TypeError, Exception):
192
+ pass
193
+
194
+ # Final fallback to default naming convention
195
+ if not cookie_name:
196
+ cookie_name = f"app_session_{self.slug_id}"
197
+
198
+ # Get cookie settings to match how it was set
199
+ should_use_secure = (
200
+ request.url.scheme == "https"
201
+ or os.getenv("G_NOME_ENV") == "production"
202
+ )
203
+
204
+ # Delete the stale cookie
205
+ response.delete_cookie(
206
+ key=cookie_name,
207
+ httponly=True,
208
+ secure=should_use_secure,
209
+ samesite="lax",
210
+ )
211
+ logger.debug(
212
+ f"Cleared stale session cookie '{cookie_name}' for {self.slug_id}"
213
+ )
214
+ except (ValueError, TypeError, AttributeError, RuntimeError) as e:
215
+ # Don't fail the request if cookie cleanup fails
216
+ logger.warning(
217
+ f"Error clearing stale session cookie for {self.slug_id}: {e}",
218
+ exc_info=True,
219
+ )
220
+
221
+ return response
222
+
223
+
224
+ def create_security_middleware(config: dict) -> Callable:
225
+ """
226
+ Create security middleware from manifest config.
227
+
228
+ Args:
229
+ config: token_management.security config from manifest
230
+
231
+ Returns:
232
+ SecurityMiddleware instance
233
+ """
234
+ security = config.get("security", {})
235
+
236
+ return SecurityMiddleware(
237
+ app=None, # Will be set by FastAPI
238
+ require_https=security.get("require_https", False),
239
+ csrf_protection=security.get("csrf_protection", True),
240
+ security_headers=True,
241
+ )