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.
- mdb_engine/__init__.py +116 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +654 -11
- mdb_engine/auth/__init__.py +136 -29
- mdb_engine/auth/audit.py +592 -0
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +265 -70
- mdb_engine/auth/config_defaults.py +5 -5
- mdb_engine/auth/config_helpers.py +19 -18
- mdb_engine/auth/cookie_utils.py +12 -16
- mdb_engine/auth/csrf.py +483 -0
- mdb_engine/auth/decorators.py +10 -16
- mdb_engine/auth/dependencies.py +69 -71
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +61 -88
- mdb_engine/auth/jwt.py +11 -15
- mdb_engine/auth/middleware.py +79 -35
- mdb_engine/auth/oso_factory.py +21 -41
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +505 -0
- mdb_engine/auth/restrictions.py +21 -36
- mdb_engine/auth/session_manager.py +24 -41
- mdb_engine/auth/shared_middleware.py +977 -0
- mdb_engine/auth/shared_users.py +775 -0
- mdb_engine/auth/token_lifecycle.py +10 -12
- mdb_engine/auth/token_store.py +17 -32
- mdb_engine/auth/users.py +99 -159
- mdb_engine/auth/utils.py +236 -42
- mdb_engine/cli/commands/generate.py +546 -10
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +7 -7
- mdb_engine/config.py +13 -28
- mdb_engine/constants.py +65 -0
- mdb_engine/core/README.md +117 -6
- mdb_engine/core/__init__.py +39 -7
- mdb_engine/core/app_registration.py +31 -50
- mdb_engine/core/app_secrets.py +289 -0
- mdb_engine/core/connection.py +20 -12
- mdb_engine/core/encryption.py +222 -0
- mdb_engine/core/engine.py +2862 -115
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +628 -204
- mdb_engine/core/ray_integration.py +436 -0
- mdb_engine/core/seeding.py +13 -21
- mdb_engine/core/service_initialization.py +20 -30
- mdb_engine/core/types.py +40 -43
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +37 -50
- mdb_engine/database/connection.py +51 -30
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +747 -237
- mdb_engine/dependencies.py +427 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +247 -0
- mdb_engine/di/providers.py +206 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +31 -24
- mdb_engine/embeddings/dependencies.py +38 -155
- mdb_engine/embeddings/service.py +78 -75
- mdb_engine/exceptions.py +104 -12
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +11 -11
- mdb_engine/indexes/manager.py +59 -123
- mdb_engine/memory/README.md +95 -4
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +363 -1168
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +17 -17
- mdb_engine/observability/logging.py +10 -10
- mdb_engine/observability/metrics.py +40 -19
- mdb_engine/repositories/__init__.py +34 -0
- mdb_engine/repositories/base.py +325 -0
- mdb_engine/repositories/mongo.py +233 -0
- mdb_engine/repositories/unit_of_work.py +166 -0
- mdb_engine/routing/README.md +1 -1
- mdb_engine/routing/__init__.py +1 -3
- mdb_engine/routing/websockets.py +41 -75
- mdb_engine/utils/__init__.py +3 -1
- mdb_engine/utils/mongo.py +117 -0
- mdb_engine-0.4.12.dist-info/METADATA +492 -0
- mdb_engine-0.4.12.dist-info/RECORD +97 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
- mdb_engine-0.1.6.dist-info/METADATA +0 -213
- mdb_engine-0.1.6.dist-info/RECORD +0 -75
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
mdb_engine/auth/dependencies.py
CHANGED
|
@@ -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
|
|
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:
|
|
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 =
|
|
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
|
-
"
|
|
47
|
-
"Set
|
|
48
|
-
"
|
|
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="
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
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:
|
|
146
|
-
) ->
|
|
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
|
|
207
|
-
logger.exception("
|
|
208
|
-
|
|
209
|
-
|
|
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) ->
|
|
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
|
|
280
|
-
logger.exception("
|
|
281
|
-
|
|
282
|
-
|
|
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:
|
|
288
|
-
) ->
|
|
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
|
|
381
|
-
logger.exception("
|
|
382
|
-
|
|
383
|
-
|
|
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:
|
|
389
|
+
user: Mapping[str, Any] | None = Depends(get_current_user),
|
|
388
390
|
authz: AuthorizationProvider = Depends(get_authz_provider),
|
|
389
|
-
) ->
|
|
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:
|
|
425
|
+
user: Mapping[str, Any] | None = Depends(get_current_user),
|
|
424
426
|
authz: AuthorizationProvider = Depends(get_authz_provider),
|
|
425
|
-
) ->
|
|
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:
|
|
485
|
-
) ->
|
|
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:
|
|
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
|
-
) ->
|
|
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:
|
|
598
|
-
device_info:
|
|
599
|
-
) ->
|
|
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
|
|
745
|
-
logger.exception("
|
|
746
|
-
|
|
747
|
-
|
|
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
|
mdb_engine/auth/helpers.py
CHANGED
|
@@ -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 (
|
|
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.
|
|
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)
|
mdb_engine/auth/integration.py
CHANGED
|
@@ -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
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
13
|
from fastapi import FastAPI
|
|
14
14
|
|
|
15
|
-
from .config_defaults import (
|
|
16
|
-
|
|
17
|
-
|
|
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:
|
|
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:
|
|
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) ->
|
|
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:
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|