mdb-engine 0.1.6__py3-none-any.whl → 0.2.0__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 +104 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +648 -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 +264 -69
- mdb_engine/auth/config_helpers.py +7 -6
- mdb_engine/auth/cookie_utils.py +3 -7
- mdb_engine/auth/csrf.py +373 -0
- mdb_engine/auth/decorators.py +3 -10
- mdb_engine/auth/dependencies.py +47 -50
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +53 -80
- mdb_engine/auth/jwt.py +2 -6
- mdb_engine/auth/middleware.py +77 -34
- mdb_engine/auth/oso_factory.py +18 -38
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +504 -0
- mdb_engine/auth/restrictions.py +8 -24
- mdb_engine/auth/session_manager.py +14 -29
- mdb_engine/auth/shared_middleware.py +600 -0
- mdb_engine/auth/shared_users.py +759 -0
- mdb_engine/auth/token_store.py +14 -28
- mdb_engine/auth/users.py +54 -113
- mdb_engine/auth/utils.py +213 -15
- mdb_engine/cli/commands/generate.py +545 -9
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +3 -3
- mdb_engine/config.py +7 -21
- 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 +22 -41
- mdb_engine/core/app_secrets.py +290 -0
- mdb_engine/core/connection.py +18 -9
- mdb_engine/core/encryption.py +223 -0
- mdb_engine/core/engine.py +1057 -93
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +459 -150
- mdb_engine/core/ray_integration.py +435 -0
- mdb_engine/core/seeding.py +10 -18
- mdb_engine/core/service_initialization.py +12 -23
- mdb_engine/core/types.py +2 -5
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +25 -37
- mdb_engine/database/connection.py +11 -18
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +713 -196
- mdb_engine/dependencies.py +426 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +248 -0
- mdb_engine/di/providers.py +205 -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 +37 -154
- mdb_engine/embeddings/service.py +11 -25
- mdb_engine/exceptions.py +92 -0
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +1 -1
- mdb_engine/indexes/manager.py +50 -114
- mdb_engine/memory/README.md +2 -2
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +30 -87
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +8 -9
- mdb_engine/observability/metrics.py +32 -12
- 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 +25 -60
- mdb_engine-0.2.0.dist-info/METADATA +313 -0
- mdb_engine-0.2.0.dist-info/RECORD +96 -0
- 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.2.0.dist-info}/WHEEL +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
mdb_engine/auth/utils.py
CHANGED
|
@@ -95,6 +95,145 @@ def get_device_info(request: Request) -> Dict[str, Any]:
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def calculate_password_entropy(password: str) -> float:
|
|
99
|
+
"""
|
|
100
|
+
Calculate the entropy of a password in bits.
|
|
101
|
+
|
|
102
|
+
Entropy measures password randomness/unpredictability.
|
|
103
|
+
Higher entropy = more secure password.
|
|
104
|
+
|
|
105
|
+
Character set sizes:
|
|
106
|
+
- Lowercase: 26
|
|
107
|
+
- Uppercase: 26
|
|
108
|
+
- Digits: 10
|
|
109
|
+
- Special: ~32
|
|
110
|
+
|
|
111
|
+
Formula: entropy = length * log2(character_set_size)
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
password: Password to calculate entropy for
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Entropy in bits (float)
|
|
118
|
+
"""
|
|
119
|
+
import math
|
|
120
|
+
|
|
121
|
+
if not password:
|
|
122
|
+
return 0.0
|
|
123
|
+
|
|
124
|
+
# Determine character set size based on what's used
|
|
125
|
+
char_set_size = 0
|
|
126
|
+
|
|
127
|
+
if re.search(r"[a-z]", password):
|
|
128
|
+
char_set_size += 26
|
|
129
|
+
if re.search(r"[A-Z]", password):
|
|
130
|
+
char_set_size += 26
|
|
131
|
+
if re.search(r"\d", password):
|
|
132
|
+
char_set_size += 10
|
|
133
|
+
if re.search(r'[!@#$%^&*(),.?":{}|<>\[\]\\;\'`~_+=\-/]', password):
|
|
134
|
+
char_set_size += 32
|
|
135
|
+
if re.search(r"\s", password):
|
|
136
|
+
char_set_size += 1
|
|
137
|
+
|
|
138
|
+
if char_set_size == 0:
|
|
139
|
+
# Fallback for Unicode or other characters
|
|
140
|
+
char_set_size = 94 # Printable ASCII assumption
|
|
141
|
+
|
|
142
|
+
# Entropy = length * log2(char_set_size)
|
|
143
|
+
entropy = len(password) * math.log2(char_set_size)
|
|
144
|
+
|
|
145
|
+
return round(entropy, 2)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_common_password(password: str) -> bool:
|
|
149
|
+
"""
|
|
150
|
+
Check if password is in the common passwords list.
|
|
151
|
+
|
|
152
|
+
Uses a bundled list of the top 10,000 most common passwords.
|
|
153
|
+
Falls back gracefully if the file is not available.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
password: Password to check
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
True if password is common, False otherwise
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
import os
|
|
163
|
+
|
|
164
|
+
# Get the path to the common passwords file
|
|
165
|
+
resources_dir = os.path.join(os.path.dirname(__file__), "resources")
|
|
166
|
+
common_passwords_path = os.path.join(resources_dir, "common_passwords.txt")
|
|
167
|
+
|
|
168
|
+
if not os.path.exists(common_passwords_path):
|
|
169
|
+
logger.debug("Common passwords file not found, skipping check")
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
# Read and check (case-insensitive)
|
|
173
|
+
password_lower = password.lower()
|
|
174
|
+
with open(common_passwords_path, encoding="utf-8") as f:
|
|
175
|
+
for line in f:
|
|
176
|
+
if line.strip().lower() == password_lower:
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
return False
|
|
180
|
+
except OSError as e:
|
|
181
|
+
logger.warning(f"Error checking common passwords: {e}")
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def check_password_breach(password: str) -> bool:
|
|
186
|
+
"""
|
|
187
|
+
Check if password has been exposed in known data breaches.
|
|
188
|
+
|
|
189
|
+
Uses the HaveIBeenPwned API with k-anonymity (only sends first 5 chars of hash).
|
|
190
|
+
This is privacy-preserving - the full password hash is never sent.
|
|
191
|
+
|
|
192
|
+
Requires network access. Returns False (not breached) if check fails.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
password: Password to check
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
True if password was found in breaches, False otherwise
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
import hashlib
|
|
202
|
+
|
|
203
|
+
import httpx
|
|
204
|
+
|
|
205
|
+
# Hash the password
|
|
206
|
+
sha1_hash = hashlib.sha1(password.encode()).hexdigest().upper()
|
|
207
|
+
prefix = sha1_hash[:5]
|
|
208
|
+
suffix = sha1_hash[5:]
|
|
209
|
+
|
|
210
|
+
# Query HIBP API (k-anonymity: only send first 5 chars)
|
|
211
|
+
url = f"https://api.pwnedpasswords.com/range/{prefix}"
|
|
212
|
+
|
|
213
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
214
|
+
response = await client.get(url)
|
|
215
|
+
response.raise_for_status()
|
|
216
|
+
|
|
217
|
+
# Check if our suffix is in the response
|
|
218
|
+
for line in response.text.splitlines():
|
|
219
|
+
hash_suffix, count = line.split(":")
|
|
220
|
+
if hash_suffix == suffix:
|
|
221
|
+
logger.debug(f"Password found in {count} breaches")
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
except ImportError:
|
|
227
|
+
logger.debug("httpx not available, skipping breach check")
|
|
228
|
+
return False
|
|
229
|
+
except httpx.HTTPError as e:
|
|
230
|
+
logger.warning(f"Error checking password breaches: {e}")
|
|
231
|
+
return False
|
|
232
|
+
except (TimeoutError, ConnectionError, OSError) as e:
|
|
233
|
+
logger.warning(f"Error checking password breaches: {e}")
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
|
|
98
237
|
def validate_password_strength(
|
|
99
238
|
password: str,
|
|
100
239
|
min_length: Optional[int] = None,
|
|
@@ -102,6 +241,8 @@ def validate_password_strength(
|
|
|
102
241
|
require_lowercase: Optional[bool] = None,
|
|
103
242
|
require_numbers: Optional[bool] = None,
|
|
104
243
|
require_special: Optional[bool] = None,
|
|
244
|
+
min_entropy_bits: Optional[int] = None,
|
|
245
|
+
check_common_passwords: Optional[bool] = None,
|
|
105
246
|
config: Optional[Dict[str, Any]] = None,
|
|
106
247
|
) -> Tuple[bool, List[str]]:
|
|
107
248
|
"""
|
|
@@ -114,6 +255,8 @@ def validate_password_strength(
|
|
|
114
255
|
require_lowercase: Require lowercase letters (default: from config or True)
|
|
115
256
|
require_numbers: Require numbers (default: from config or True)
|
|
116
257
|
require_special: Require special characters (default: from config or False)
|
|
258
|
+
min_entropy_bits: Minimum entropy in bits (default: from config or 0)
|
|
259
|
+
check_common_passwords: Check against common passwords (default: from config or False)
|
|
117
260
|
config: Optional password_policy config dict from manifest
|
|
118
261
|
|
|
119
262
|
Returns:
|
|
@@ -125,9 +268,7 @@ def validate_password_strength(
|
|
|
125
268
|
return False, ["Password is required"]
|
|
126
269
|
|
|
127
270
|
if config:
|
|
128
|
-
min_length = (
|
|
129
|
-
min_length if min_length is not None else config.get("min_length", 8)
|
|
130
|
-
)
|
|
271
|
+
min_length = min_length if min_length is not None else config.get("min_length", 8)
|
|
131
272
|
require_uppercase = (
|
|
132
273
|
require_uppercase
|
|
133
274
|
if require_uppercase is not None
|
|
@@ -139,14 +280,18 @@ def validate_password_strength(
|
|
|
139
280
|
else config.get("require_lowercase", True)
|
|
140
281
|
)
|
|
141
282
|
require_numbers = (
|
|
142
|
-
require_numbers
|
|
143
|
-
if require_numbers is not None
|
|
144
|
-
else config.get("require_numbers", True)
|
|
283
|
+
require_numbers if require_numbers is not None else config.get("require_numbers", True)
|
|
145
284
|
)
|
|
146
285
|
require_special = (
|
|
147
|
-
require_special
|
|
148
|
-
|
|
149
|
-
|
|
286
|
+
require_special if require_special is not None else config.get("require_special", False)
|
|
287
|
+
)
|
|
288
|
+
min_entropy_bits = (
|
|
289
|
+
min_entropy_bits if min_entropy_bits is not None else config.get("min_entropy_bits", 0)
|
|
290
|
+
)
|
|
291
|
+
check_common_passwords = (
|
|
292
|
+
check_common_passwords
|
|
293
|
+
if check_common_passwords is not None
|
|
294
|
+
else config.get("check_common_passwords", False)
|
|
150
295
|
)
|
|
151
296
|
else:
|
|
152
297
|
min_length = min_length if min_length is not None else 8
|
|
@@ -154,6 +299,10 @@ def validate_password_strength(
|
|
|
154
299
|
require_lowercase = require_lowercase if require_lowercase is not None else True
|
|
155
300
|
require_numbers = require_numbers if require_numbers is not None else True
|
|
156
301
|
require_special = require_special if require_special is not None else False
|
|
302
|
+
min_entropy_bits = min_entropy_bits if min_entropy_bits is not None else 0
|
|
303
|
+
check_common_passwords = (
|
|
304
|
+
check_common_passwords if check_common_passwords is not None else False
|
|
305
|
+
)
|
|
157
306
|
|
|
158
307
|
if len(password) < min_length:
|
|
159
308
|
errors.append(f"Password must be at least {min_length} characters long")
|
|
@@ -170,9 +319,62 @@ def validate_password_strength(
|
|
|
170
319
|
if require_special and not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
|
171
320
|
errors.append("Password must contain at least one special character")
|
|
172
321
|
|
|
322
|
+
# Entropy check
|
|
323
|
+
if min_entropy_bits and min_entropy_bits > 0:
|
|
324
|
+
entropy = calculate_password_entropy(password)
|
|
325
|
+
if entropy < min_entropy_bits:
|
|
326
|
+
errors.append(
|
|
327
|
+
f"Password is too weak (entropy: {entropy:.0f} bits, "
|
|
328
|
+
f"required: {min_entropy_bits} bits)"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Common password check
|
|
332
|
+
if check_common_passwords:
|
|
333
|
+
if is_common_password(password):
|
|
334
|
+
errors.append("Password is too common - please choose a unique password")
|
|
335
|
+
|
|
173
336
|
return len(errors) == 0, errors
|
|
174
337
|
|
|
175
338
|
|
|
339
|
+
async def validate_password_strength_async(
|
|
340
|
+
password: str,
|
|
341
|
+
config: Optional[Dict[str, Any]] = None,
|
|
342
|
+
check_breaches: Optional[bool] = None,
|
|
343
|
+
) -> Tuple[bool, List[str]]:
|
|
344
|
+
"""
|
|
345
|
+
Async version of validate_password_strength with breach checking.
|
|
346
|
+
|
|
347
|
+
This performs all synchronous checks, plus an optional async breach
|
|
348
|
+
check against HaveIBeenPwned.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
password: Password to validate
|
|
352
|
+
config: Optional password_policy config dict from manifest
|
|
353
|
+
check_breaches: Check against HIBP breach database (default: from config or False)
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Tuple of (is_valid, list_of_errors)
|
|
357
|
+
"""
|
|
358
|
+
# First, run sync validation
|
|
359
|
+
is_valid, errors = validate_password_strength(password, config=config)
|
|
360
|
+
|
|
361
|
+
# Determine if we should check breaches
|
|
362
|
+
if check_breaches is None and config:
|
|
363
|
+
check_breaches = config.get("check_breaches", False)
|
|
364
|
+
elif check_breaches is None:
|
|
365
|
+
check_breaches = False
|
|
366
|
+
|
|
367
|
+
# Async breach check
|
|
368
|
+
if check_breaches:
|
|
369
|
+
if await check_password_breach(password):
|
|
370
|
+
errors.append(
|
|
371
|
+
"Password has been exposed in a data breach - " "please choose a different password"
|
|
372
|
+
)
|
|
373
|
+
is_valid = False
|
|
374
|
+
|
|
375
|
+
return is_valid, errors
|
|
376
|
+
|
|
377
|
+
|
|
176
378
|
def generate_session_fingerprint(request: Request, device_id: str) -> str:
|
|
177
379
|
"""
|
|
178
380
|
Generate a session fingerprint from request characteristics.
|
|
@@ -478,9 +680,7 @@ async def register_user(
|
|
|
478
680
|
)
|
|
479
681
|
|
|
480
682
|
response = _create_registration_response(user_doc, redirect_url)
|
|
481
|
-
set_auth_cookies(
|
|
482
|
-
response, access_token, refresh_token, request=request, config=config
|
|
483
|
-
)
|
|
683
|
+
set_auth_cookies(response, access_token, refresh_token, request=request, config=config)
|
|
484
684
|
|
|
485
685
|
response.set_cookie(
|
|
486
686
|
key="device_id",
|
|
@@ -510,9 +710,7 @@ async def register_user(
|
|
|
510
710
|
return {"success": False, "error": "Registration failed. Please try again."}
|
|
511
711
|
|
|
512
712
|
|
|
513
|
-
async def _get_user_id_from_request(
|
|
514
|
-
request: Request, user_id: Optional[str]
|
|
515
|
-
) -> Optional[str]:
|
|
713
|
+
async def _get_user_id_from_request(request: Request, user_id: Optional[str]) -> Optional[str]:
|
|
516
714
|
"""Extract user_id from request if not provided."""
|
|
517
715
|
if user_id:
|
|
518
716
|
return user_id
|