mdb-engine 0.2.1__py3-none-any.whl → 0.2.3__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/auth/audit.py +40 -40
- mdb_engine/auth/base.py +3 -3
- mdb_engine/auth/casbin_factory.py +6 -6
- mdb_engine/auth/config_defaults.py +5 -5
- mdb_engine/auth/config_helpers.py +12 -12
- mdb_engine/auth/cookie_utils.py +9 -9
- mdb_engine/auth/csrf.py +9 -8
- mdb_engine/auth/decorators.py +7 -6
- mdb_engine/auth/dependencies.py +22 -21
- mdb_engine/auth/integration.py +9 -9
- mdb_engine/auth/jwt.py +9 -9
- mdb_engine/auth/middleware.py +4 -3
- mdb_engine/auth/oso_factory.py +6 -6
- mdb_engine/auth/provider.py +4 -4
- mdb_engine/auth/rate_limiter.py +12 -11
- mdb_engine/auth/restrictions.py +16 -15
- mdb_engine/auth/session_manager.py +11 -13
- mdb_engine/auth/shared_middleware.py +16 -15
- mdb_engine/auth/shared_users.py +20 -20
- mdb_engine/auth/token_lifecycle.py +10 -12
- mdb_engine/auth/token_store.py +4 -5
- mdb_engine/auth/users.py +51 -52
- mdb_engine/auth/utils.py +29 -33
- mdb_engine/cli/commands/generate.py +6 -6
- mdb_engine/cli/utils.py +4 -4
- mdb_engine/config.py +6 -7
- mdb_engine/core/app_registration.py +12 -12
- mdb_engine/core/app_secrets.py +1 -2
- mdb_engine/core/connection.py +3 -4
- mdb_engine/core/encryption.py +1 -2
- mdb_engine/core/engine.py +43 -44
- mdb_engine/core/manifest.py +59 -58
- mdb_engine/core/ray_integration.py +10 -9
- mdb_engine/core/seeding.py +3 -3
- mdb_engine/core/service_initialization.py +10 -9
- mdb_engine/core/types.py +40 -40
- mdb_engine/database/abstraction.py +15 -16
- mdb_engine/database/connection.py +40 -12
- mdb_engine/database/query_validator.py +8 -8
- mdb_engine/database/resource_limiter.py +7 -7
- mdb_engine/database/scoped_wrapper.py +51 -58
- mdb_engine/dependencies.py +14 -13
- mdb_engine/di/container.py +12 -13
- mdb_engine/di/providers.py +14 -13
- mdb_engine/di/scopes.py +5 -5
- mdb_engine/embeddings/dependencies.py +2 -2
- mdb_engine/embeddings/service.py +31 -43
- mdb_engine/exceptions.py +20 -20
- mdb_engine/indexes/helpers.py +11 -11
- mdb_engine/indexes/manager.py +9 -9
- mdb_engine/memory/service.py +30 -30
- mdb_engine/observability/health.py +10 -9
- mdb_engine/observability/logging.py +10 -10
- mdb_engine/observability/metrics.py +8 -7
- mdb_engine/repositories/base.py +25 -25
- mdb_engine/repositories/mongo.py +17 -17
- mdb_engine/repositories/unit_of_work.py +6 -6
- mdb_engine/routing/websockets.py +19 -18
- {mdb_engine-0.2.1.dist-info → mdb_engine-0.2.3.dist-info}/METADATA +8 -8
- mdb_engine-0.2.3.dist-info/RECORD +96 -0
- mdb_engine-0.2.1.dist-info/RECORD +0 -96
- {mdb_engine-0.2.1.dist-info → mdb_engine-0.2.3.dist-info}/WHEEL +0 -0
- {mdb_engine-0.2.1.dist-info → mdb_engine-0.2.3.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.2.1.dist-info → mdb_engine-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.2.1.dist-info → mdb_engine-0.2.3.dist-info}/top_level.txt +0 -0
mdb_engine/auth/dependencies.py
CHANGED
|
@@ -9,8 +9,9 @@ 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
|
|
@@ -26,7 +27,7 @@ from .token_store import TokenBlacklist
|
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
29
|
-
_SECRET_KEY_CACHE:
|
|
30
|
+
_SECRET_KEY_CACHE: str | None = None
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def _get_secret_key() -> str:
|
|
@@ -94,7 +95,7 @@ def _get_secret_key_value() -> str:
|
|
|
94
95
|
SECRET_KEY = _SecretKey()
|
|
95
96
|
|
|
96
97
|
|
|
97
|
-
def _validate_next_url(next_url:
|
|
98
|
+
def _validate_next_url(next_url: str | None) -> str:
|
|
98
99
|
"""
|
|
99
100
|
Sanitizes a 'next' URL parameter to prevent Open Redirect vulnerabilities.
|
|
100
101
|
"""
|
|
@@ -127,7 +128,7 @@ async def get_authz_provider(request: Request) -> AuthorizationProvider:
|
|
|
127
128
|
return provider
|
|
128
129
|
|
|
129
130
|
|
|
130
|
-
async def get_token_blacklist(request: Request) ->
|
|
131
|
+
async def get_token_blacklist(request: Request) -> TokenBlacklist | None:
|
|
131
132
|
"""
|
|
132
133
|
FastAPI Dependency: Retrieves token blacklist from app.state.
|
|
133
134
|
|
|
@@ -137,7 +138,7 @@ async def get_token_blacklist(request: Request) -> Optional[TokenBlacklist]:
|
|
|
137
138
|
return blacklist
|
|
138
139
|
|
|
139
140
|
|
|
140
|
-
async def get_session_manager(request: Request) ->
|
|
141
|
+
async def get_session_manager(request: Request) -> SessionManager | None:
|
|
141
142
|
"""
|
|
142
143
|
FastAPI Dependency: Retrieves session manager from app.state.
|
|
143
144
|
|
|
@@ -149,8 +150,8 @@ async def get_session_manager(request: Request) -> Optional[SessionManager]:
|
|
|
149
150
|
|
|
150
151
|
async def get_current_user(
|
|
151
152
|
request: Request,
|
|
152
|
-
token:
|
|
153
|
-
) ->
|
|
153
|
+
token: str | None = Cookie(default=None),
|
|
154
|
+
) -> dict[str, Any] | None:
|
|
154
155
|
"""
|
|
155
156
|
FastAPI Dependency: Decodes and validates the JWT stored in the 'token' cookie.
|
|
156
157
|
|
|
@@ -212,7 +213,7 @@ async def get_current_user(
|
|
|
212
213
|
return None
|
|
213
214
|
|
|
214
215
|
|
|
215
|
-
async def get_current_user_from_request(request: Request) ->
|
|
216
|
+
async def get_current_user_from_request(request: Request) -> dict[str, Any] | None:
|
|
216
217
|
"""
|
|
217
218
|
Helper function to get current user from a Request object.
|
|
218
219
|
This is useful when you need to call get_current_user outside of FastAPI dependency injection.
|
|
@@ -289,8 +290,8 @@ async def get_current_user_from_request(request: Request) -> Optional[Dict[str,
|
|
|
289
290
|
|
|
290
291
|
async def get_refresh_token(
|
|
291
292
|
request: Request,
|
|
292
|
-
refresh_token:
|
|
293
|
-
) ->
|
|
293
|
+
refresh_token: str | None = Cookie(default=None),
|
|
294
|
+
) -> dict[str, Any] | None:
|
|
294
295
|
"""
|
|
295
296
|
FastAPI Dependency: Validates refresh token from cookie.
|
|
296
297
|
|
|
@@ -385,9 +386,9 @@ async def get_refresh_token(
|
|
|
385
386
|
|
|
386
387
|
|
|
387
388
|
async def require_admin(
|
|
388
|
-
user:
|
|
389
|
+
user: Mapping[str, Any] | None = Depends(get_current_user),
|
|
389
390
|
authz: AuthorizationProvider = Depends(get_authz_provider),
|
|
390
|
-
) ->
|
|
391
|
+
) -> dict[str, Any]:
|
|
391
392
|
"""
|
|
392
393
|
FastAPI Dependency: Enforces admin privileges via the pluggable AuthZ provider.
|
|
393
394
|
"""
|
|
@@ -421,9 +422,9 @@ async def require_admin(
|
|
|
421
422
|
|
|
422
423
|
|
|
423
424
|
async def require_admin_or_developer(
|
|
424
|
-
user:
|
|
425
|
+
user: Mapping[str, Any] | None = Depends(get_current_user),
|
|
425
426
|
authz: AuthorizationProvider = Depends(get_authz_provider),
|
|
426
|
-
) ->
|
|
427
|
+
) -> dict[str, Any]:
|
|
427
428
|
"""
|
|
428
429
|
FastAPI Dependency: Enforces admin OR developer privileges.
|
|
429
430
|
Developers can upload apps, admins can upload any app.
|
|
@@ -482,8 +483,8 @@ async def require_admin_or_developer(
|
|
|
482
483
|
|
|
483
484
|
|
|
484
485
|
async def get_current_user_or_redirect(
|
|
485
|
-
request: Request, user:
|
|
486
|
-
) ->
|
|
486
|
+
request: Request, user: Mapping[str, Any] | None = Depends(get_current_user)
|
|
487
|
+
) -> dict[str, Any]:
|
|
487
488
|
"""
|
|
488
489
|
FastAPI Dependency: Enforces user authentication. Redirects to login if not authenticated.
|
|
489
490
|
"""
|
|
@@ -534,10 +535,10 @@ def require_permission(obj: str, act: str, force_login: bool = True):
|
|
|
534
535
|
|
|
535
536
|
async def _check_permission(
|
|
536
537
|
# 2. The type hint MUST be Optional now
|
|
537
|
-
user:
|
|
538
|
+
user: dict[str, Any] | None = Depends(user_dependency),
|
|
538
539
|
# 3. Ask for the generic INTERFACE
|
|
539
540
|
authz: AuthorizationProvider = Depends(get_authz_provider),
|
|
540
|
-
) ->
|
|
541
|
+
) -> dict[str, Any] | None: # 4. Return type is also Optional
|
|
541
542
|
"""Internal dependency function performing the AuthZ check."""
|
|
542
543
|
|
|
543
544
|
# 5. Check for 'anonymous' if user is None
|
|
@@ -595,9 +596,9 @@ def require_permission(obj: str, act: str, force_login: bool = True):
|
|
|
595
596
|
|
|
596
597
|
async def refresh_access_token(
|
|
597
598
|
request: Request,
|
|
598
|
-
refresh_token_payload:
|
|
599
|
-
device_info:
|
|
600
|
-
) ->
|
|
599
|
+
refresh_token_payload: dict[str, Any],
|
|
600
|
+
device_info: dict[str, Any] | None = None,
|
|
601
|
+
) -> tuple[str, str, dict[str, Any]] | None:
|
|
601
602
|
"""
|
|
602
603
|
Refresh an access token using a valid refresh token.
|
|
603
604
|
|
mdb_engine/auth/integration.py
CHANGED
|
@@ -8,7 +8,7 @@ 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
|
|
|
@@ -24,7 +24,7 @@ from .helpers import initialize_token_management
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
26
|
# Cache for auth configs
|
|
27
|
-
_auth_config_cache:
|
|
27
|
+
_auth_config_cache: dict[str, dict[str, Any]] = {}
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _has_cors_middleware(app: FastAPI) -> bool:
|
|
@@ -57,7 +57,7 @@ def _has_cors_middleware(app: FastAPI) -> bool:
|
|
|
57
57
|
return False
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
def invalidate_auth_config_cache(slug_id:
|
|
60
|
+
def invalidate_auth_config_cache(slug_id: str | None = None) -> None:
|
|
61
61
|
"""
|
|
62
62
|
Invalidate auth config cache for a specific app or all apps.
|
|
63
63
|
|
|
@@ -72,7 +72,7 @@ def invalidate_auth_config_cache(slug_id: Optional[str] = None) -> None:
|
|
|
72
72
|
logger.debug("Invalidated entire auth config cache")
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
async def get_auth_config(slug_id: str, engine) ->
|
|
75
|
+
async def get_auth_config(slug_id: str, engine) -> dict[str, Any]:
|
|
76
76
|
"""
|
|
77
77
|
Retrieve authentication configuration from manifest.
|
|
78
78
|
|
|
@@ -121,7 +121,7 @@ async def get_auth_config(slug_id: str, engine) -> Dict[str, Any]:
|
|
|
121
121
|
|
|
122
122
|
|
|
123
123
|
async def _setup_authorization_provider(
|
|
124
|
-
app: FastAPI, engine, slug_id: str, config:
|
|
124
|
+
app: FastAPI, engine, slug_id: str, config: dict[str, Any]
|
|
125
125
|
) -> None:
|
|
126
126
|
"""Set up authorization provider (Casbin/OSO/custom) from manifest."""
|
|
127
127
|
auth = config.get("auth", {})
|
|
@@ -181,7 +181,7 @@ async def _setup_authorization_provider(
|
|
|
181
181
|
logger.info(f"Custom provider specified for {slug_id} - manual setup required")
|
|
182
182
|
|
|
183
183
|
|
|
184
|
-
async def _setup_demo_users(app: FastAPI, engine, slug_id: str, config:
|
|
184
|
+
async def _setup_demo_users(app: FastAPI, engine, slug_id: str, config: dict[str, Any]) -> list:
|
|
185
185
|
"""Set up demo users and link with OSO roles if applicable."""
|
|
186
186
|
auth = config.get("auth", {})
|
|
187
187
|
users_config = auth.get("users", {})
|
|
@@ -325,7 +325,7 @@ async def _setup_demo_users(app: FastAPI, engine, slug_id: str, config: Dict[str
|
|
|
325
325
|
|
|
326
326
|
|
|
327
327
|
async def _setup_token_management(
|
|
328
|
-
app: FastAPI, engine, slug_id: str, token_management:
|
|
328
|
+
app: FastAPI, engine, slug_id: str, token_management: dict[str, Any]
|
|
329
329
|
) -> None:
|
|
330
330
|
"""Initialize token management (blacklist and session manager)."""
|
|
331
331
|
if token_management.get("auto_setup", True):
|
|
@@ -356,7 +356,7 @@ async def _setup_token_management(
|
|
|
356
356
|
|
|
357
357
|
|
|
358
358
|
async def _setup_security_middleware(
|
|
359
|
-
app: FastAPI, slug_id: str, security_config:
|
|
359
|
+
app: FastAPI, slug_id: str, security_config: dict[str, Any]
|
|
360
360
|
) -> None:
|
|
361
361
|
"""Set up security middleware (if not already added)."""
|
|
362
362
|
if security_config.get("csrf_protection", True) or security_config.get("require_https", False):
|
|
@@ -392,7 +392,7 @@ async def _setup_security_middleware(
|
|
|
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
|
mdb_engine/auth/jwt.py
CHANGED
|
@@ -10,7 +10,7 @@ This module is part of MDB_ENGINE - MongoDB Engine.
|
|
|
10
10
|
import logging
|
|
11
11
|
import uuid
|
|
12
12
|
from datetime import datetime, timedelta
|
|
13
|
-
from typing import Any
|
|
13
|
+
from typing import Any
|
|
14
14
|
|
|
15
15
|
import jwt
|
|
16
16
|
|
|
@@ -21,7 +21,7 @@ from ..constants import CURRENT_TOKEN_VERSION
|
|
|
21
21
|
logger = logging.getLogger(__name__)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def decode_jwt_token(token: Any, secret_key: str) ->
|
|
24
|
+
def decode_jwt_token(token: Any, secret_key: str) -> dict[str, Any]:
|
|
25
25
|
"""
|
|
26
26
|
Helper function to decode JWT tokens with automatic fallback to bytes format.
|
|
27
27
|
|
|
@@ -74,7 +74,7 @@ def decode_jwt_token(token: Any, secret_key: str) -> Dict[str, Any]:
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def encode_jwt_token(
|
|
77
|
-
payload:
|
|
77
|
+
payload: dict[str, Any], secret_key: str, expires_in: int | None = None
|
|
78
78
|
) -> str:
|
|
79
79
|
"""
|
|
80
80
|
Encode a JWT token with enhanced claims.
|
|
@@ -123,12 +123,12 @@ def encode_jwt_token(
|
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def generate_token_pair(
|
|
126
|
-
user_data:
|
|
126
|
+
user_data: dict[str, Any],
|
|
127
127
|
secret_key: str,
|
|
128
|
-
device_info:
|
|
129
|
-
access_token_ttl:
|
|
130
|
-
refresh_token_ttl:
|
|
131
|
-
) ->
|
|
128
|
+
device_info: dict[str, Any] | None = None,
|
|
129
|
+
access_token_ttl: int | None = None,
|
|
130
|
+
refresh_token_ttl: int | None = None,
|
|
131
|
+
) -> tuple[str, str, dict[str, Any]]:
|
|
132
132
|
"""
|
|
133
133
|
Generate a pair of access and refresh tokens.
|
|
134
134
|
|
|
@@ -190,7 +190,7 @@ def generate_token_pair(
|
|
|
190
190
|
return access_token, refresh_token, token_metadata
|
|
191
191
|
|
|
192
192
|
|
|
193
|
-
def extract_token_metadata(token: str, secret_key: str) ->
|
|
193
|
+
def extract_token_metadata(token: str, secret_key: str) -> dict[str, Any] | None:
|
|
194
194
|
"""
|
|
195
195
|
Extract metadata from a token without full validation.
|
|
196
196
|
|
mdb_engine/auth/middleware.py
CHANGED
|
@@ -15,7 +15,8 @@ Security Features:
|
|
|
15
15
|
import logging
|
|
16
16
|
import os
|
|
17
17
|
import secrets
|
|
18
|
-
from
|
|
18
|
+
from collections.abc import Awaitable, Callable
|
|
19
|
+
from typing import Any
|
|
19
20
|
|
|
20
21
|
from fastapi import HTTPException, Request, Response, status
|
|
21
22
|
from fastapi.responses import RedirectResponse
|
|
@@ -53,7 +54,7 @@ class SecurityMiddleware(BaseHTTPMiddleware):
|
|
|
53
54
|
require_https: bool = False,
|
|
54
55
|
csrf_protection: bool = True,
|
|
55
56
|
security_headers: bool = True,
|
|
56
|
-
hsts_config:
|
|
57
|
+
hsts_config: dict[str, Any] | None = None,
|
|
57
58
|
):
|
|
58
59
|
"""
|
|
59
60
|
Initialize security middleware.
|
|
@@ -234,7 +235,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
|
|
|
234
235
|
"session_cookie_name", "app_session"
|
|
235
236
|
)
|
|
236
237
|
cookie_name = f"{session_cookie_name}_{self.slug_id}"
|
|
237
|
-
except (AttributeError, KeyError, TypeError
|
|
238
|
+
except (AttributeError, KeyError, TypeError):
|
|
238
239
|
pass
|
|
239
240
|
|
|
240
241
|
# Final fallback to default naming convention
|
mdb_engine/auth/oso_factory.py
CHANGED
|
@@ -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
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from .provider import OsoAdapter
|
|
@@ -20,8 +20,8 @@ logger = logging.getLogger(__name__)
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
async def create_oso_cloud_client(
|
|
23
|
-
api_key:
|
|
24
|
-
url:
|
|
23
|
+
api_key: str | None = None,
|
|
24
|
+
url: str | None = None,
|
|
25
25
|
max_retries: int = 3,
|
|
26
26
|
retry_delay: float = 2.0,
|
|
27
27
|
) -> Any:
|
|
@@ -114,8 +114,8 @@ async def create_oso_cloud_client(
|
|
|
114
114
|
|
|
115
115
|
async def setup_initial_oso_facts(
|
|
116
116
|
authz_provider: OsoAdapter,
|
|
117
|
-
initial_roles:
|
|
118
|
-
initial_policies:
|
|
117
|
+
initial_roles: list[dict[str, Any]] | None = None,
|
|
118
|
+
initial_policies: list[dict[str, Any]] | None = None,
|
|
119
119
|
) -> None:
|
|
120
120
|
"""
|
|
121
121
|
Set up initial roles and policies in OSO Cloud.
|
|
@@ -149,7 +149,7 @@ async def setup_initial_oso_facts(
|
|
|
149
149
|
|
|
150
150
|
async def initialize_oso_from_manifest(
|
|
151
151
|
engine, app_slug: str, auth_config: dict[str, Any]
|
|
152
|
-
) ->
|
|
152
|
+
) -> OsoAdapter | None:
|
|
153
153
|
"""
|
|
154
154
|
Initialize OSO Cloud provider from manifest configuration.
|
|
155
155
|
|
mdb_engine/auth/provider.py
CHANGED
|
@@ -11,7 +11,7 @@ from __future__ import annotations # MUST be first import for string type hints
|
|
|
11
11
|
import asyncio
|
|
12
12
|
import logging
|
|
13
13
|
import time
|
|
14
|
-
from typing import TYPE_CHECKING, Any,
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
15
15
|
|
|
16
16
|
from ..constants import AUTHZ_CACHE_TTL, MAX_CACHE_SIZE
|
|
17
17
|
|
|
@@ -38,7 +38,7 @@ class AuthorizationProvider(Protocol):
|
|
|
38
38
|
subject: str,
|
|
39
39
|
resource: str,
|
|
40
40
|
action: str,
|
|
41
|
-
user_object:
|
|
41
|
+
user_object: dict[str, Any] | None = None,
|
|
42
42
|
) -> bool:
|
|
43
43
|
"""
|
|
44
44
|
Checks if a subject is allowed to perform an action on a resource.
|
|
@@ -91,7 +91,7 @@ class CasbinAdapter(BaseAuthorizationProvider):
|
|
|
91
91
|
subject: str,
|
|
92
92
|
resource: str,
|
|
93
93
|
action: str,
|
|
94
|
-
user_object:
|
|
94
|
+
user_object: dict[str, Any] | None = None,
|
|
95
95
|
) -> bool:
|
|
96
96
|
"""
|
|
97
97
|
Check authorization using Casbin's enforce method.
|
|
@@ -321,7 +321,7 @@ class OsoAdapter(BaseAuthorizationProvider):
|
|
|
321
321
|
subject: str,
|
|
322
322
|
resource: str,
|
|
323
323
|
action: str,
|
|
324
|
-
user_object:
|
|
324
|
+
user_object: dict[str, Any] | None = None,
|
|
325
325
|
) -> bool:
|
|
326
326
|
"""
|
|
327
327
|
Check authorization using OSO's authorize method.
|
mdb_engine/auth/rate_limiter.py
CHANGED
|
@@ -33,10 +33,11 @@ Usage:
|
|
|
33
33
|
import logging
|
|
34
34
|
import time
|
|
35
35
|
from collections import defaultdict
|
|
36
|
+
from collections.abc import Callable
|
|
36
37
|
from dataclasses import dataclass
|
|
37
38
|
from datetime import datetime, timedelta
|
|
38
39
|
from functools import wraps
|
|
39
|
-
from typing import Any
|
|
40
|
+
from typing import Any
|
|
40
41
|
|
|
41
42
|
from pymongo.errors import OperationFailure
|
|
42
43
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
@@ -53,7 +54,7 @@ class RateLimit:
|
|
|
53
54
|
max_attempts: int = 5
|
|
54
55
|
window_seconds: int = 300 # 5 minutes
|
|
55
56
|
|
|
56
|
-
def to_dict(self) ->
|
|
57
|
+
def to_dict(self) -> dict[str, int]:
|
|
57
58
|
return {
|
|
58
59
|
"max_attempts": self.max_attempts,
|
|
59
60
|
"window_seconds": self.window_seconds,
|
|
@@ -61,7 +62,7 @@ class RateLimit:
|
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
# Default rate limits for auth endpoints
|
|
64
|
-
DEFAULT_AUTH_RATE_LIMITS:
|
|
65
|
+
DEFAULT_AUTH_RATE_LIMITS: dict[str, RateLimit] = {
|
|
65
66
|
"/login": RateLimit(max_attempts=5, window_seconds=300),
|
|
66
67
|
"/register": RateLimit(max_attempts=3, window_seconds=3600),
|
|
67
68
|
"/logout": RateLimit(max_attempts=10, window_seconds=60),
|
|
@@ -78,7 +79,7 @@ class InMemoryRateLimitStore:
|
|
|
78
79
|
|
|
79
80
|
def __init__(self):
|
|
80
81
|
# Structure: {identifier: [(timestamp, count), ...]}
|
|
81
|
-
self._storage:
|
|
82
|
+
self._storage: dict[str, list[tuple[float, int]]] = defaultdict(list)
|
|
82
83
|
|
|
83
84
|
async def record_attempt(
|
|
84
85
|
self,
|
|
@@ -282,8 +283,8 @@ class AuthRateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
282
283
|
def __init__(
|
|
283
284
|
self,
|
|
284
285
|
app: Callable,
|
|
285
|
-
limits:
|
|
286
|
-
store:
|
|
286
|
+
limits: dict[str, RateLimit] | None = None,
|
|
287
|
+
store: InMemoryRateLimitStore | None = None,
|
|
287
288
|
include_email_in_key: bool = True,
|
|
288
289
|
):
|
|
289
290
|
"""
|
|
@@ -377,7 +378,7 @@ class AuthRateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
377
378
|
|
|
378
379
|
return "unknown"
|
|
379
380
|
|
|
380
|
-
async def _extract_email(self, request: Request) ->
|
|
381
|
+
async def _extract_email(self, request: Request) -> str | None:
|
|
381
382
|
"""Try to extract email from request body."""
|
|
382
383
|
try:
|
|
383
384
|
# Only try to read body for JSON requests
|
|
@@ -410,8 +411,8 @@ class AuthRateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
410
411
|
|
|
411
412
|
|
|
412
413
|
def create_rate_limit_middleware(
|
|
413
|
-
manifest_auth:
|
|
414
|
-
store:
|
|
414
|
+
manifest_auth: dict[str, Any],
|
|
415
|
+
store: InMemoryRateLimitStore | None = None,
|
|
415
416
|
) -> type:
|
|
416
417
|
"""
|
|
417
418
|
Factory function to create rate limit middleware from manifest config.
|
|
@@ -435,7 +436,7 @@ def create_rate_limit_middleware(
|
|
|
435
436
|
"""
|
|
436
437
|
rate_limits_config = manifest_auth.get("rate_limits", {})
|
|
437
438
|
|
|
438
|
-
limits:
|
|
439
|
+
limits: dict[str, RateLimit] = {}
|
|
439
440
|
for path, config in rate_limits_config.items():
|
|
440
441
|
limits[path] = RateLimit(
|
|
441
442
|
max_attempts=config.get("max_attempts", 5),
|
|
@@ -456,7 +457,7 @@ def create_rate_limit_middleware(
|
|
|
456
457
|
def rate_limit(
|
|
457
458
|
max_attempts: int = 5,
|
|
458
459
|
window_seconds: int = 300,
|
|
459
|
-
key_func:
|
|
460
|
+
key_func: Callable[[Request], str] | None = None,
|
|
460
461
|
):
|
|
461
462
|
"""
|
|
462
463
|
Decorator for rate limiting individual endpoints.
|
mdb_engine/auth/restrictions.py
CHANGED
|
@@ -16,7 +16,8 @@ This module is part of MDB_ENGINE - MongoDB Engine.
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
import logging
|
|
19
|
-
from
|
|
19
|
+
from collections.abc import Awaitable, Callable
|
|
20
|
+
from typing import Any
|
|
20
21
|
|
|
21
22
|
from fastapi import HTTPException, Request, status
|
|
22
23
|
|
|
@@ -27,7 +28,7 @@ from .users import get_app_user
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
29
30
|
|
|
30
|
-
def is_demo_user(user:
|
|
31
|
+
def is_demo_user(user: dict[str, Any] | None = None, email: str | None = None) -> bool:
|
|
31
32
|
"""
|
|
32
33
|
Check if a user is a demo user.
|
|
33
34
|
|
|
@@ -55,7 +56,7 @@ def is_demo_user(user: Optional[Dict[str, Any]] = None, email: Optional[str] = N
|
|
|
55
56
|
return False
|
|
56
57
|
|
|
57
58
|
|
|
58
|
-
async def _get_platform_user(request: Request) ->
|
|
59
|
+
async def _get_platform_user(request: Request) -> dict[str, Any] | None:
|
|
59
60
|
"""Try to get user from platform authentication."""
|
|
60
61
|
try:
|
|
61
62
|
platform_user = await get_current_user_from_request(request)
|
|
@@ -70,9 +71,9 @@ async def _get_platform_user(request: Request) -> Optional[Dict[str, Any]]:
|
|
|
70
71
|
async def _get_sub_auth_user(
|
|
71
72
|
request: Request,
|
|
72
73
|
slug_id: str,
|
|
73
|
-
get_app_config_func: Callable[[Request, str,
|
|
74
|
+
get_app_config_func: Callable[[Request, str, dict], Awaitable[dict]],
|
|
74
75
|
get_app_db_func: Callable[[Request], Awaitable[Any]],
|
|
75
|
-
) ->
|
|
76
|
+
) -> dict[str, Any] | None:
|
|
76
77
|
"""Try to get user from sub-authentication."""
|
|
77
78
|
try:
|
|
78
79
|
db = await get_app_db_func(request)
|
|
@@ -104,9 +105,9 @@ async def _get_sub_auth_user(
|
|
|
104
105
|
async def _get_authenticated_user(
|
|
105
106
|
request: Request,
|
|
106
107
|
slug_id: str,
|
|
107
|
-
get_app_config_func:
|
|
108
|
-
get_app_db_func:
|
|
109
|
-
) ->
|
|
108
|
+
get_app_config_func: Callable[[Request, str, dict], Awaitable[dict]] | None,
|
|
109
|
+
get_app_db_func: Callable[[Request], Awaitable[Any]] | None,
|
|
110
|
+
) -> dict[str, Any] | None:
|
|
110
111
|
"""Get authenticated user from platform or sub-auth."""
|
|
111
112
|
# Try platform auth first
|
|
112
113
|
user = await _get_platform_user(request)
|
|
@@ -132,8 +133,8 @@ def _validate_slug_id(request: Request) -> str:
|
|
|
132
133
|
|
|
133
134
|
|
|
134
135
|
def _validate_dependencies(
|
|
135
|
-
get_app_config_func:
|
|
136
|
-
get_app_db_func:
|
|
136
|
+
get_app_config_func: Callable[[Request, str, dict], Awaitable[dict]] | None,
|
|
137
|
+
get_app_db_func: Callable[[Request], Awaitable[Any]] | None,
|
|
137
138
|
) -> None:
|
|
138
139
|
"""Validate that required dependencies are provided."""
|
|
139
140
|
if not get_app_db_func:
|
|
@@ -152,9 +153,9 @@ def _validate_dependencies(
|
|
|
152
153
|
|
|
153
154
|
async def require_non_demo_user(
|
|
154
155
|
request: Request,
|
|
155
|
-
get_app_config_func:
|
|
156
|
-
get_app_db_func:
|
|
157
|
-
) ->
|
|
156
|
+
get_app_config_func: Callable[[Request, str, dict], Awaitable[dict]] | None = None,
|
|
157
|
+
get_app_db_func: Callable[[Request], Awaitable[Any]] | None = None,
|
|
158
|
+
) -> dict[str, Any]:
|
|
158
159
|
"""
|
|
159
160
|
FastAPI dependency that blocks demo users from accessing an endpoint.
|
|
160
161
|
|
|
@@ -205,8 +206,8 @@ async def require_non_demo_user(
|
|
|
205
206
|
|
|
206
207
|
async def block_demo_users(
|
|
207
208
|
request: Request,
|
|
208
|
-
get_app_config_func:
|
|
209
|
-
get_app_db_func:
|
|
209
|
+
get_app_config_func: Callable[[Request, str, dict], Awaitable[dict]] | None = None,
|
|
210
|
+
get_app_db_func: Callable[[Request], Awaitable[Any]] | None = None,
|
|
210
211
|
):
|
|
211
212
|
"""
|
|
212
213
|
FastAPI dependency that blocks demo users and returns an error response.
|
|
@@ -8,7 +8,7 @@ This module is part of MDB_ENGINE - MongoDB Engine.
|
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
10
|
from datetime import datetime, timedelta
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
13
|
from bson.objectid import ObjectId
|
|
14
14
|
|
|
@@ -96,10 +96,10 @@ class SessionManager:
|
|
|
96
96
|
user_id: str,
|
|
97
97
|
device_id: str,
|
|
98
98
|
refresh_jti: str,
|
|
99
|
-
device_info:
|
|
100
|
-
ip_address:
|
|
101
|
-
session_fingerprint:
|
|
102
|
-
) ->
|
|
99
|
+
device_info: dict[str, Any] | None = None,
|
|
100
|
+
ip_address: str | None = None,
|
|
101
|
+
session_fingerprint: str | None = None,
|
|
102
|
+
) -> dict[str, Any] | None:
|
|
103
103
|
"""
|
|
104
104
|
Create a new user session.
|
|
105
105
|
|
|
@@ -173,7 +173,7 @@ class SessionManager:
|
|
|
173
173
|
return None
|
|
174
174
|
|
|
175
175
|
async def update_session_activity(
|
|
176
|
-
self, refresh_jti: str, ip_address:
|
|
176
|
+
self, refresh_jti: str, ip_address: str | None = None
|
|
177
177
|
) -> bool:
|
|
178
178
|
"""
|
|
179
179
|
Update session last_seen timestamp (activity tracking).
|
|
@@ -207,7 +207,7 @@ class SessionManager:
|
|
|
207
207
|
logger.error(f"Error updating session activity for {refresh_jti}: {e}", exc_info=True)
|
|
208
208
|
return False
|
|
209
209
|
|
|
210
|
-
async def get_session_by_refresh_token(self, refresh_jti: str) ->
|
|
210
|
+
async def get_session_by_refresh_token(self, refresh_jti: str) -> dict[str, Any] | None:
|
|
211
211
|
"""
|
|
212
212
|
Get session by refresh token JWT ID.
|
|
213
213
|
|
|
@@ -234,7 +234,7 @@ class SessionManager:
|
|
|
234
234
|
return None
|
|
235
235
|
|
|
236
236
|
async def validate_session_fingerprint(
|
|
237
|
-
self, refresh_jti: str, current_fingerprint: str, strict:
|
|
237
|
+
self, refresh_jti: str, current_fingerprint: str, strict: bool | None = None
|
|
238
238
|
) -> bool:
|
|
239
239
|
"""
|
|
240
240
|
Validate session fingerprint matches stored fingerprint.
|
|
@@ -288,7 +288,7 @@ class SessionManager:
|
|
|
288
288
|
|
|
289
289
|
async def get_user_sessions(
|
|
290
290
|
self, user_id: str, active_only: bool = True
|
|
291
|
-
) ->
|
|
291
|
+
) -> list[dict[str, Any]]:
|
|
292
292
|
"""
|
|
293
293
|
Get all sessions for a user.
|
|
294
294
|
|
|
@@ -354,9 +354,7 @@ class SessionManager:
|
|
|
354
354
|
logger.error(f"Error revoking session {session_id}: {e}", exc_info=True)
|
|
355
355
|
return False
|
|
356
356
|
|
|
357
|
-
async def revoke_user_sessions(
|
|
358
|
-
self, user_id: str, exclude_device_id: Optional[str] = None
|
|
359
|
-
) -> int:
|
|
357
|
+
async def revoke_user_sessions(self, user_id: str, exclude_device_id: str | None = None) -> int:
|
|
360
358
|
"""
|
|
361
359
|
Revoke all sessions for a user.
|
|
362
360
|
|
|
@@ -390,7 +388,7 @@ class SessionManager:
|
|
|
390
388
|
logger.error(f"Error revoking sessions for user {user_id}: {e}", exc_info=True)
|
|
391
389
|
return 0
|
|
392
390
|
|
|
393
|
-
async def cleanup_inactive_sessions(self, user_id:
|
|
391
|
+
async def cleanup_inactive_sessions(self, user_id: str | None = None) -> int:
|
|
394
392
|
"""
|
|
395
393
|
Clean up inactive sessions (beyond inactivity timeout).
|
|
396
394
|
|