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
@@ -0,0 +1,158 @@
1
+ """
2
+ Cookie Security Utilities
3
+
4
+ Provides secure cookie configuration helpers based on manifest settings and environment.
5
+
6
+ This module is part of MDB_ENGINE - MongoDB Engine.
7
+ """
8
+
9
+ import logging
10
+ import os
11
+ from typing import Any, Dict, Optional
12
+
13
+ from fastapi import Request
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def get_secure_cookie_settings(
19
+ request: Request, config: Optional[Dict[str, Any]] = None
20
+ ) -> Dict[str, Any]:
21
+ """
22
+ Get secure cookie settings based on manifest config and request environment.
23
+
24
+ Args:
25
+ request: FastAPI Request object
26
+ config: Optional token_management config from manifest (if None, uses defaults)
27
+
28
+ Returns:
29
+ Dictionary of cookie settings for FastAPI response.set_cookie()
30
+ """
31
+ # Default settings
32
+ secure = False
33
+ httponly = True
34
+ samesite = "lax"
35
+
36
+ # Get security config from token_management
37
+ if config:
38
+ security = config.get("security", {})
39
+
40
+ # HttpOnly flag
41
+ httponly = security.get("cookie_httponly", True)
42
+
43
+ # SameSite flag
44
+ samesite_str = security.get("cookie_samesite", "lax")
45
+ samesite = samesite_str.lower()
46
+
47
+ # Secure flag - determine based on config and environment
48
+ cookie_secure = security.get("cookie_secure", "auto")
49
+
50
+ if cookie_secure == "auto":
51
+ # Auto-detect: secure if HTTPS or production environment
52
+ is_https = request.url.scheme == "https"
53
+ is_production = (
54
+ os.getenv("G_NOME_ENV") == "production"
55
+ or os.getenv("ENVIRONMENT") == "production"
56
+ )
57
+ secure = is_https or is_production
58
+ elif cookie_secure == "true":
59
+ secure = True
60
+ else:
61
+ secure = False
62
+ else:
63
+ # No config - use environment-based defaults
64
+ is_https = request.url.scheme == "https"
65
+ is_production = (
66
+ os.getenv("G_NOME_ENV") == "production"
67
+ or os.getenv("ENVIRONMENT") == "production"
68
+ )
69
+ secure = is_https or is_production
70
+
71
+ return {
72
+ "httponly": httponly,
73
+ "secure": secure,
74
+ "samesite": samesite,
75
+ }
76
+
77
+
78
+ def set_auth_cookies(
79
+ response,
80
+ access_token: str,
81
+ refresh_token: Optional[str] = None,
82
+ request: Optional[Request] = None,
83
+ config: Optional[Dict[str, Any]] = None,
84
+ access_token_ttl: Optional[int] = None,
85
+ refresh_token_ttl: Optional[int] = None,
86
+ ):
87
+ """
88
+ Set authentication cookies on a response with secure settings.
89
+
90
+ Args:
91
+ response: FastAPI Response object
92
+ access_token: Access token to set in cookie
93
+ refresh_token: Optional refresh token to set in cookie
94
+ request: Optional Request object for environment detection
95
+ config: Optional token_management config from manifest
96
+ access_token_ttl: Optional access token TTL in seconds (from config if not provided)
97
+ refresh_token_ttl: Optional refresh token TTL in seconds (from config if not provided)
98
+ """
99
+ # Get cookie settings
100
+ if request:
101
+ cookie_settings = get_secure_cookie_settings(request, config)
102
+ else:
103
+ cookie_settings = {
104
+ "httponly": True,
105
+ "secure": os.getenv("G_NOME_ENV") == "production",
106
+ "samesite": "lax",
107
+ }
108
+
109
+ # Get TTLs
110
+ if access_token_ttl is None and config:
111
+ access_token_ttl = config.get("access_token_ttl", 900)
112
+ elif access_token_ttl is None:
113
+ access_token_ttl = 900 # Default 15 minutes
114
+
115
+ if refresh_token_ttl is None and config:
116
+ refresh_token_ttl = config.get("refresh_token_ttl", 604800)
117
+ elif refresh_token_ttl is None:
118
+ refresh_token_ttl = 604800 # Default 7 days
119
+
120
+ # Set access token cookie
121
+ response.set_cookie(
122
+ key="token", value=access_token, max_age=access_token_ttl, **cookie_settings
123
+ )
124
+
125
+ # Set refresh token cookie if provided
126
+ if refresh_token:
127
+ response.set_cookie(
128
+ key="refresh_token",
129
+ value=refresh_token,
130
+ max_age=refresh_token_ttl,
131
+ **cookie_settings,
132
+ )
133
+
134
+
135
+ def clear_auth_cookies(response, request: Optional[Request] = None):
136
+ """
137
+ Clear authentication cookies from response.
138
+
139
+ Args:
140
+ response: FastAPI Response object
141
+ request: Optional Request object for environment detection
142
+ """
143
+ # Get cookie settings for samesite (needed for deletion)
144
+ if request:
145
+ cookie_settings = get_secure_cookie_settings(request)
146
+ samesite = cookie_settings.get("samesite", "lax")
147
+ secure = cookie_settings.get("secure", False)
148
+ else:
149
+ samesite = "lax"
150
+ secure = os.getenv("G_NOME_ENV") == "production"
151
+
152
+ # Delete access token cookie
153
+ response.delete_cookie(key="token", httponly=True, secure=secure, samesite=samesite)
154
+
155
+ # Delete refresh token cookie
156
+ response.delete_cookie(
157
+ key="refresh_token", httponly=True, secure=secure, samesite=samesite
158
+ )
@@ -0,0 +1,350 @@
1
+ """
2
+ Authentication Decorators
3
+
4
+ Decorators for simplifying authentication and security enforcement.
5
+
6
+ This module is part of MDB_ENGINE - MongoDB Engine.
7
+ """
8
+
9
+ import logging
10
+ import time
11
+ from collections import defaultdict
12
+ from functools import wraps
13
+ from typing import Any, Awaitable, Callable, Dict, Optional
14
+
15
+ from fastapi import HTTPException, Request, status
16
+ from fastapi.responses import RedirectResponse
17
+
18
+ from .dependencies import get_current_user_from_request
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Rate limiting storage (in-memory, can be replaced with Redis for distributed systems)
23
+ _rate_limit_storage: Dict[str, Dict[str, Any]] = defaultdict(dict)
24
+
25
+
26
+ def require_auth(redirect_to: str = "/login"):
27
+ """
28
+ Decorator for routes requiring authentication.
29
+
30
+ Automatically injects user into request.state.user and redirects to login if not authenticated.
31
+
32
+ Usage:
33
+ @app.get("/dashboard")
34
+ @require_auth()
35
+ async def dashboard(request: Request):
36
+ user = request.state.user # Automatically available
37
+ ...
38
+
39
+ Args:
40
+ redirect_to: URL to redirect to if not authenticated (default: "/login")
41
+ """
42
+
43
+ def decorator(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
44
+ @wraps(func)
45
+ async def wrapper(request: Request, *args, **kwargs):
46
+ user = await get_current_user_from_request(request)
47
+ if not user:
48
+ # Check if it's an API request (JSON) or web request
49
+ accept = request.headers.get("accept", "")
50
+ if "application/json" in accept:
51
+ raise HTTPException(
52
+ status_code=status.HTTP_401_UNAUTHORIZED,
53
+ detail="Authentication required",
54
+ )
55
+ else:
56
+ # Web request - redirect
57
+ return RedirectResponse(url=redirect_to, status_code=302)
58
+
59
+ # Inject user into request state
60
+ request.state.user = user
61
+
62
+ return await func(request, *args, **kwargs)
63
+
64
+ return wrapper
65
+
66
+ return decorator
67
+
68
+
69
+ def _is_production_environment() -> bool:
70
+ """Check if running in production environment."""
71
+ import os
72
+
73
+ return (
74
+ os.getenv("G_NOME_ENV") == "production"
75
+ or os.getenv("ENVIRONMENT") == "production"
76
+ )
77
+
78
+
79
+ def _validate_https(request: Request) -> None:
80
+ """Validate HTTPS requirement in production."""
81
+ if _is_production_environment() and request.url.scheme != "https":
82
+ raise HTTPException(
83
+ status_code=status.HTTP_403_FORBIDDEN,
84
+ detail="HTTPS required in production",
85
+ )
86
+
87
+
88
+ async def _get_csrf_token(request: Request) -> Optional[str]:
89
+ """Extract CSRF token from request headers or form data."""
90
+ csrf_token = request.headers.get("X-CSRF-Token")
91
+ if csrf_token:
92
+ return csrf_token
93
+
94
+ # Try to get from form data if not in headers
95
+ try:
96
+ form_data = await request.form()
97
+ return form_data.get("csrf_token")
98
+ except (RuntimeError, ValueError):
99
+ # Type 2: Recoverable - form parsing failed, return None
100
+ return None
101
+
102
+
103
+ def _is_state_changing_method(method: str) -> bool:
104
+ """Check if HTTP method is state-changing."""
105
+ return method in ["POST", "PUT", "DELETE", "PATCH"]
106
+
107
+
108
+ async def _validate_csrf_token(request: Request) -> None:
109
+ """Validate CSRF token for state-changing requests."""
110
+ csrf_token = await _get_csrf_token(request)
111
+ session_csrf = request.cookies.get("csrf_token")
112
+
113
+ # Only validate CSRF if a session token exists
114
+ # If no session token exists yet (e.g., first registration), allow the request
115
+ if session_csrf and (not csrf_token or csrf_token != session_csrf):
116
+ raise HTTPException(
117
+ status_code=status.HTTP_403_FORBIDDEN,
118
+ detail="Invalid or missing CSRF token",
119
+ )
120
+
121
+
122
+ def token_security(enforce_https: bool = True, check_csrf: bool = True):
123
+ """
124
+ Decorator to enforce security settings from manifest.
125
+
126
+ Validates HTTPS in production, CSRF tokens, and secure cookie enforcement.
127
+
128
+ Usage:
129
+ @app.post("/api/data")
130
+ @token_security()
131
+ async def update_data(request: Request):
132
+ ...
133
+
134
+ Args:
135
+ enforce_https: Enforce HTTPS in production (default: True)
136
+ check_csrf: Check CSRF tokens (default: True)
137
+ """
138
+
139
+ def decorator(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
140
+ @wraps(func)
141
+ async def wrapper(request: Request, *args, **kwargs):
142
+ if enforce_https:
143
+ _validate_https(request)
144
+
145
+ if check_csrf and _is_state_changing_method(request.method):
146
+ await _validate_csrf_token(request)
147
+
148
+ return await func(request, *args, **kwargs)
149
+
150
+ return wrapper
151
+
152
+ return decorator
153
+
154
+
155
+ def rate_limit_auth(
156
+ endpoint: str = "login",
157
+ max_attempts: Optional[int] = None,
158
+ window_seconds: Optional[int] = None,
159
+ ):
160
+ """
161
+ Rate limiting decorator for auth endpoints.
162
+
163
+ Tracks attempts by IP + email and returns 429 when exceeded.
164
+ If max_attempts/window_seconds not provided, reads from manifest config.
165
+
166
+ Usage:
167
+ @app.post("/login")
168
+ @rate_limit_auth(endpoint="login")
169
+ async def login(request: Request, email: str, password: str):
170
+ ...
171
+
172
+ Args:
173
+ endpoint: Endpoint identifier for rate limiting (default: "login")
174
+ max_attempts: Maximum attempts allowed (default: from manifest config or 5)
175
+ window_seconds: Time window in seconds (default: from manifest config or 300)
176
+ """
177
+
178
+ def decorator(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
179
+ @wraps(func)
180
+ async def wrapper(request: Request, *args, **kwargs):
181
+ # Get rate limit config from manifest if available
182
+ config = getattr(request.state, "token_management_config", None)
183
+ rate_limit_config = None
184
+
185
+ if config:
186
+ security = config.get("security", {})
187
+ rate_limiting = security.get("rate_limiting", {})
188
+ rate_limit_config = rate_limiting.get(endpoint)
189
+
190
+ # Use provided values or config values or defaults
191
+ if max_attempts is None:
192
+ max_attempts_val = (
193
+ rate_limit_config.get("max_attempts") if rate_limit_config else 5
194
+ )
195
+ else:
196
+ max_attempts_val = max_attempts
197
+
198
+ if window_seconds is None:
199
+ window_seconds_val = (
200
+ rate_limit_config.get("window_seconds")
201
+ if rate_limit_config
202
+ else 300
203
+ )
204
+ else:
205
+ window_seconds_val = window_seconds
206
+
207
+ # Get identifier (IP + email if available)
208
+ ip_address = request.client.host if request.client else "unknown"
209
+ email = kwargs.get("email") or (
210
+ await request.form() if request.method == "POST" else {}
211
+ ).get("email", "")
212
+
213
+ identifier = f"{endpoint}:{ip_address}:{email}"
214
+ current_time = time.time()
215
+
216
+ # Clean old entries
217
+ if identifier in _rate_limit_storage:
218
+ attempts = _rate_limit_storage[identifier]
219
+ # Remove old attempts outside window
220
+ _rate_limit_storage[identifier] = {
221
+ ts: count
222
+ for ts, count in attempts.items()
223
+ if current_time - ts < window_seconds_val
224
+ }
225
+
226
+ # Count attempts in window
227
+ attempts_in_window = sum(_rate_limit_storage[identifier].values())
228
+
229
+ if attempts_in_window >= max_attempts_val:
230
+ logger.warning(
231
+ f"Rate limit exceeded for {identifier}: "
232
+ f"{attempts_in_window} attempts in {window_seconds_val}s"
233
+ )
234
+ raise HTTPException(
235
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
236
+ detail=f"Too many attempts. Please try again in {window_seconds_val} seconds.",
237
+ headers={"Retry-After": str(window_seconds_val)},
238
+ )
239
+
240
+ # Record this attempt
241
+ if identifier not in _rate_limit_storage:
242
+ _rate_limit_storage[identifier] = {}
243
+ _rate_limit_storage[identifier][current_time] = 1
244
+
245
+ return await func(request, *args, **kwargs)
246
+
247
+ return wrapper
248
+
249
+ return decorator
250
+
251
+
252
+ def auto_token_setup(func: Optional[Callable[..., Awaitable[Any]]] = None):
253
+ """
254
+ Decorator to automatically set up tokens on successful login/register.
255
+
256
+ This decorator wraps login/register functions and automatically:
257
+ - Generates token pair
258
+ - Sets cookies with correct security settings
259
+ - Creates session if enabled
260
+ - Reads config from manifest
261
+
262
+ Usage:
263
+ @app.post("/login")
264
+ @auto_token_setup
265
+ async def login(request: Request, email: str, password: str):
266
+ # Your login logic that returns user dict
267
+ user = await authenticate_user(email, password)
268
+ return {"user": user} # Decorator handles token setup
269
+ """
270
+
271
+ def decorator(f: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
272
+ @wraps(f)
273
+ async def wrapper(request: Request, *args, **kwargs):
274
+ # Call original function
275
+ result = await f(request, *args, **kwargs)
276
+
277
+ # If result contains user, set up tokens
278
+ if isinstance(result, dict) and "user" in result:
279
+ try:
280
+ from .cookie_utils import set_auth_cookies
281
+ from .dependencies import SECRET_KEY, get_session_manager
282
+ from .jwt import generate_token_pair
283
+ from .utils import get_device_info
284
+
285
+ user = result["user"]
286
+ user_data = {
287
+ "user_id": str(user.get("_id") or user.get("user_id")),
288
+ "email": user.get("email"),
289
+ }
290
+
291
+ # Get device info
292
+ device_info = get_device_info(request)
293
+
294
+ # Generate token pair
295
+ access_token, refresh_token, token_metadata = generate_token_pair(
296
+ user_data, str(SECRET_KEY), device_info=device_info
297
+ )
298
+
299
+ # Create session if available
300
+ session_mgr = await get_session_manager(request)
301
+ if session_mgr:
302
+ await session_mgr.create_session(
303
+ user_id=user_data["email"],
304
+ device_id=device_info["device_id"],
305
+ refresh_jti=token_metadata.get("refresh_jti"),
306
+ device_info=device_info,
307
+ ip_address=device_info.get("ip_address"),
308
+ )
309
+
310
+ # Get config from request state or manifest
311
+ config = getattr(request.state, "token_management_config", None)
312
+
313
+ # Create response if not already a response
314
+ if not hasattr(result, "set_cookie"):
315
+ from fastapi.responses import JSONResponse
316
+
317
+ response = JSONResponse(result)
318
+ else:
319
+ response = result
320
+
321
+ # Set cookies
322
+ set_auth_cookies(
323
+ response,
324
+ access_token,
325
+ refresh_token,
326
+ request=request,
327
+ config=config,
328
+ )
329
+
330
+ return response
331
+ except (
332
+ ValueError,
333
+ TypeError,
334
+ AttributeError,
335
+ KeyError,
336
+ RuntimeError,
337
+ ) as e:
338
+ logger.error(f"Error in auto_token_setup: {e}", exc_info=True)
339
+ # Return original result if token setup fails
340
+ return result
341
+
342
+ return result
343
+
344
+ return wrapper
345
+
346
+ # Support both @auto_token_setup and @auto_token_setup()
347
+ if func is None:
348
+ return decorator
349
+ else:
350
+ return decorator(func)