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.
- mdb_engine/README.md +144 -0
- mdb_engine/__init__.py +37 -0
- mdb_engine/auth/README.md +631 -0
- mdb_engine/auth/__init__.py +128 -0
- mdb_engine/auth/casbin_factory.py +199 -0
- mdb_engine/auth/casbin_models.py +46 -0
- mdb_engine/auth/config_defaults.py +71 -0
- mdb_engine/auth/config_helpers.py +213 -0
- mdb_engine/auth/cookie_utils.py +158 -0
- mdb_engine/auth/decorators.py +350 -0
- mdb_engine/auth/dependencies.py +747 -0
- mdb_engine/auth/helpers.py +64 -0
- mdb_engine/auth/integration.py +578 -0
- mdb_engine/auth/jwt.py +225 -0
- mdb_engine/auth/middleware.py +241 -0
- mdb_engine/auth/oso_factory.py +323 -0
- mdb_engine/auth/provider.py +570 -0
- mdb_engine/auth/restrictions.py +271 -0
- mdb_engine/auth/session_manager.py +477 -0
- mdb_engine/auth/token_lifecycle.py +213 -0
- mdb_engine/auth/token_store.py +289 -0
- mdb_engine/auth/users.py +1516 -0
- mdb_engine/auth/utils.py +614 -0
- mdb_engine/cli/__init__.py +13 -0
- mdb_engine/cli/commands/__init__.py +7 -0
- mdb_engine/cli/commands/generate.py +105 -0
- mdb_engine/cli/commands/migrate.py +83 -0
- mdb_engine/cli/commands/show.py +70 -0
- mdb_engine/cli/commands/validate.py +63 -0
- mdb_engine/cli/main.py +41 -0
- mdb_engine/cli/utils.py +92 -0
- mdb_engine/config.py +217 -0
- mdb_engine/constants.py +160 -0
- mdb_engine/core/README.md +542 -0
- mdb_engine/core/__init__.py +42 -0
- mdb_engine/core/app_registration.py +392 -0
- mdb_engine/core/connection.py +243 -0
- mdb_engine/core/engine.py +749 -0
- mdb_engine/core/index_management.py +162 -0
- mdb_engine/core/manifest.py +2793 -0
- mdb_engine/core/seeding.py +179 -0
- mdb_engine/core/service_initialization.py +355 -0
- mdb_engine/core/types.py +413 -0
- mdb_engine/database/README.md +522 -0
- mdb_engine/database/__init__.py +31 -0
- mdb_engine/database/abstraction.py +635 -0
- mdb_engine/database/connection.py +387 -0
- mdb_engine/database/scoped_wrapper.py +1721 -0
- mdb_engine/embeddings/README.md +184 -0
- mdb_engine/embeddings/__init__.py +62 -0
- mdb_engine/embeddings/dependencies.py +193 -0
- mdb_engine/embeddings/service.py +759 -0
- mdb_engine/exceptions.py +167 -0
- mdb_engine/indexes/README.md +651 -0
- mdb_engine/indexes/__init__.py +21 -0
- mdb_engine/indexes/helpers.py +145 -0
- mdb_engine/indexes/manager.py +895 -0
- mdb_engine/memory/README.md +451 -0
- mdb_engine/memory/__init__.py +30 -0
- mdb_engine/memory/service.py +1285 -0
- mdb_engine/observability/README.md +515 -0
- mdb_engine/observability/__init__.py +42 -0
- mdb_engine/observability/health.py +296 -0
- mdb_engine/observability/logging.py +161 -0
- mdb_engine/observability/metrics.py +297 -0
- mdb_engine/routing/README.md +462 -0
- mdb_engine/routing/__init__.py +73 -0
- mdb_engine/routing/websockets.py +813 -0
- mdb_engine/utils/__init__.py +7 -0
- mdb_engine-0.1.6.dist-info/METADATA +213 -0
- mdb_engine-0.1.6.dist-info/RECORD +75 -0
- mdb_engine-0.1.6.dist-info/WHEEL +5 -0
- mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
- mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
- 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
|
+
)
|