mdb-engine 0.1.7__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 +71 -10
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +125 -11
- mdb_engine/auth/__init__.py +7 -1
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +258 -59
- mdb_engine/auth/dependencies.py +10 -5
- mdb_engine/auth/integration.py +23 -7
- mdb_engine/auth/oso_factory.py +2 -2
- mdb_engine/auth/provider.py +263 -143
- mdb_engine/core/engine.py +307 -6
- mdb_engine/core/manifest.py +35 -15
- mdb_engine/database/README.md +28 -1
- 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 +22 -23
- mdb_engine/embeddings/dependencies.py +37 -152
- 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-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/METADATA +42 -14
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/RECORD +31 -20
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
mdb_engine/core/engine.py
CHANGED
|
@@ -30,7 +30,7 @@ import os
|
|
|
30
30
|
import secrets
|
|
31
31
|
from contextlib import asynccontextmanager
|
|
32
32
|
from pathlib import Path
|
|
33
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
|
|
33
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Tuple
|
|
34
34
|
|
|
35
35
|
from motor.motor_asyncio import AsyncIOMotorClient
|
|
36
36
|
from pymongo.errors import PyMongoError
|
|
@@ -280,7 +280,21 @@ class MongoDBEngine:
|
|
|
280
280
|
|
|
281
281
|
@property
|
|
282
282
|
def _initialized(self) -> bool:
|
|
283
|
-
"""Check if engine is initialized."""
|
|
283
|
+
"""Check if engine is initialized (internal)."""
|
|
284
|
+
return self._connection_manager.initialized
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def initialized(self) -> bool:
|
|
288
|
+
"""
|
|
289
|
+
Check if engine is initialized.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
True if the engine has been initialized, False otherwise.
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
if engine.initialized:
|
|
296
|
+
db = engine.get_scoped_db("my_app")
|
|
297
|
+
"""
|
|
284
298
|
return self._connection_manager.initialized
|
|
285
299
|
|
|
286
300
|
def get_scoped_db(
|
|
@@ -861,6 +875,30 @@ class MongoDBEngine:
|
|
|
861
875
|
return self._service_initializer.get_memory_service(slug)
|
|
862
876
|
return None
|
|
863
877
|
|
|
878
|
+
def get_embedding_service(self, slug: str) -> Optional[Any]:
|
|
879
|
+
"""
|
|
880
|
+
Get EmbeddingService for an app.
|
|
881
|
+
|
|
882
|
+
Auto-detects OpenAI or AzureOpenAI from environment variables.
|
|
883
|
+
Uses embedding_config from manifest.json if available.
|
|
884
|
+
|
|
885
|
+
Args:
|
|
886
|
+
slug: App slug
|
|
887
|
+
|
|
888
|
+
Returns:
|
|
889
|
+
EmbeddingService instance if embedding is enabled for this app, None otherwise
|
|
890
|
+
|
|
891
|
+
Example:
|
|
892
|
+
```python
|
|
893
|
+
embedding_service = engine.get_embedding_service("my_app")
|
|
894
|
+
if embedding_service:
|
|
895
|
+
vectors = await embedding_service.embed_chunks(["Hello world"])
|
|
896
|
+
```
|
|
897
|
+
"""
|
|
898
|
+
from ..embeddings.dependencies import get_embedding_service_for_app
|
|
899
|
+
|
|
900
|
+
return get_embedding_service_for_app(slug, self)
|
|
901
|
+
|
|
864
902
|
@property
|
|
865
903
|
def _apps(self) -> Dict[str, Any]:
|
|
866
904
|
"""
|
|
@@ -1052,6 +1090,12 @@ class MongoDBEngine:
|
|
|
1052
1090
|
slug: str,
|
|
1053
1091
|
manifest: Path,
|
|
1054
1092
|
title: Optional[str] = None,
|
|
1093
|
+
on_startup: Optional[
|
|
1094
|
+
Callable[["FastAPI", "MongoDBEngine", Dict[str, Any]], Awaitable[None]]
|
|
1095
|
+
] = None,
|
|
1096
|
+
on_shutdown: Optional[
|
|
1097
|
+
Callable[["FastAPI", "MongoDBEngine", Dict[str, Any]], Awaitable[None]]
|
|
1098
|
+
] = None,
|
|
1055
1099
|
**fastapi_kwargs: Any,
|
|
1056
1100
|
) -> "FastAPI":
|
|
1057
1101
|
"""
|
|
@@ -1065,20 +1109,33 @@ class MongoDBEngine:
|
|
|
1065
1109
|
- "app" (default): Per-app token authentication
|
|
1066
1110
|
- "shared": Shared user pool with SSO, auto-adds SharedAuthMiddleware
|
|
1067
1111
|
5. Auto-retrieves app tokens (for "app" mode)
|
|
1068
|
-
6.
|
|
1112
|
+
6. Calls on_startup callback (if provided)
|
|
1113
|
+
7. Shuts down the engine on shutdown (calls on_shutdown first if provided)
|
|
1069
1114
|
|
|
1070
1115
|
Args:
|
|
1071
1116
|
slug: Application slug (must match manifest slug)
|
|
1072
1117
|
manifest: Path to manifest.json file
|
|
1073
1118
|
title: FastAPI app title. Defaults to app name from manifest
|
|
1119
|
+
on_startup: Optional async callback called after engine initialization.
|
|
1120
|
+
Signature: async def callback(app, engine, manifest) -> None
|
|
1121
|
+
on_shutdown: Optional async callback called before engine shutdown.
|
|
1122
|
+
Signature: async def callback(app, engine, manifest) -> None
|
|
1074
1123
|
**fastapi_kwargs: Additional arguments passed to FastAPI()
|
|
1075
1124
|
|
|
1076
1125
|
Returns:
|
|
1077
1126
|
Configured FastAPI application
|
|
1078
1127
|
|
|
1079
1128
|
Example:
|
|
1129
|
+
async def my_startup(app, engine, manifest):
|
|
1130
|
+
db = engine.get_scoped_db("my_app")
|
|
1131
|
+
await db.config.insert_one({"initialized": True})
|
|
1132
|
+
|
|
1080
1133
|
engine = MongoDBEngine(mongo_uri=..., db_name=...)
|
|
1081
|
-
app = engine.create_app(
|
|
1134
|
+
app = engine.create_app(
|
|
1135
|
+
slug="my_app",
|
|
1136
|
+
manifest=Path("manifest.json"),
|
|
1137
|
+
on_startup=my_startup,
|
|
1138
|
+
)
|
|
1082
1139
|
|
|
1083
1140
|
@app.get("/")
|
|
1084
1141
|
async def index():
|
|
@@ -1151,12 +1208,181 @@ class MongoDBEngine:
|
|
|
1151
1208
|
logger.info(f"Shared auth mode for '{slug}' - SSO enabled")
|
|
1152
1209
|
# Initialize shared user pool and set on app.state
|
|
1153
1210
|
# Middleware was already added at app creation time (lazy version)
|
|
1154
|
-
await engine._initialize_shared_user_pool(app)
|
|
1211
|
+
await engine._initialize_shared_user_pool(app, app_manifest)
|
|
1155
1212
|
else:
|
|
1156
1213
|
logger.info(f"Per-app auth mode for '{slug}'")
|
|
1157
1214
|
# Auto-retrieve app token for "app" mode
|
|
1158
1215
|
await engine.auto_retrieve_app_token(slug)
|
|
1159
1216
|
|
|
1217
|
+
# Auto-initialize authorization provider from manifest config
|
|
1218
|
+
try:
|
|
1219
|
+
logger.info(
|
|
1220
|
+
f"🔍 Checking auth config for '{slug}': "
|
|
1221
|
+
f"auth_config keys={list(auth_config.keys())}"
|
|
1222
|
+
)
|
|
1223
|
+
auth_policy = auth_config.get("policy", {})
|
|
1224
|
+
logger.info(f"🔍 Auth policy for '{slug}': {auth_policy}")
|
|
1225
|
+
authz_provider_type = auth_policy.get("provider")
|
|
1226
|
+
logger.info(f"🔍 Authz provider type for '{slug}': {authz_provider_type}")
|
|
1227
|
+
except (KeyError, AttributeError, TypeError) as e:
|
|
1228
|
+
logger.exception(f"❌ Error reading auth config for '{slug}': {e}")
|
|
1229
|
+
authz_provider_type = None
|
|
1230
|
+
|
|
1231
|
+
if authz_provider_type == "oso":
|
|
1232
|
+
# Initialize OSO Cloud provider
|
|
1233
|
+
try:
|
|
1234
|
+
from ..auth.oso_factory import initialize_oso_from_manifest
|
|
1235
|
+
|
|
1236
|
+
authz_provider = await initialize_oso_from_manifest(engine, slug, app_manifest)
|
|
1237
|
+
if authz_provider:
|
|
1238
|
+
app.state.authz_provider = authz_provider
|
|
1239
|
+
logger.info(f"✅ OSO Cloud provider auto-initialized for '{slug}'")
|
|
1240
|
+
else:
|
|
1241
|
+
logger.warning(
|
|
1242
|
+
f"⚠️ OSO provider not initialized for '{slug}' - "
|
|
1243
|
+
"check OSO_AUTH and OSO_URL environment variables"
|
|
1244
|
+
)
|
|
1245
|
+
except ImportError as e:
|
|
1246
|
+
logger.warning(
|
|
1247
|
+
f"⚠️ OSO Cloud SDK not available for '{slug}': {e}. "
|
|
1248
|
+
"Install with: pip install oso-cloud"
|
|
1249
|
+
)
|
|
1250
|
+
except (ValueError, TypeError, RuntimeError, AttributeError, KeyError) as e:
|
|
1251
|
+
logger.exception(f"❌ Failed to initialize OSO provider for '{slug}': {e}")
|
|
1252
|
+
|
|
1253
|
+
elif authz_provider_type == "casbin":
|
|
1254
|
+
# Initialize Casbin provider
|
|
1255
|
+
logger.info(f"🔧 Initializing Casbin provider for '{slug}'...")
|
|
1256
|
+
try:
|
|
1257
|
+
from ..auth.casbin_factory import initialize_casbin_from_manifest
|
|
1258
|
+
|
|
1259
|
+
logger.debug(f"Calling initialize_casbin_from_manifest for '{slug}'")
|
|
1260
|
+
authz_provider = await initialize_casbin_from_manifest(
|
|
1261
|
+
engine, slug, app_manifest
|
|
1262
|
+
)
|
|
1263
|
+
logger.debug(
|
|
1264
|
+
f"initialize_casbin_from_manifest returned: {authz_provider is not None}"
|
|
1265
|
+
)
|
|
1266
|
+
if authz_provider:
|
|
1267
|
+
app.state.authz_provider = authz_provider
|
|
1268
|
+
logger.info(
|
|
1269
|
+
f"✅ Casbin provider auto-initialized for '{slug}' "
|
|
1270
|
+
f"and set on app.state"
|
|
1271
|
+
)
|
|
1272
|
+
logger.info(
|
|
1273
|
+
f"✅ Provider type: {type(authz_provider).__name__}, "
|
|
1274
|
+
f"initialized: {getattr(authz_provider, '_initialized', 'unknown')}"
|
|
1275
|
+
)
|
|
1276
|
+
# Verify it's actually set
|
|
1277
|
+
if hasattr(app.state, "authz_provider") and app.state.authz_provider:
|
|
1278
|
+
logger.info("✅ Verified: app.state.authz_provider is set and not None")
|
|
1279
|
+
else:
|
|
1280
|
+
logger.error(
|
|
1281
|
+
"❌ CRITICAL: app.state.authz_provider was set but is now "
|
|
1282
|
+
"None or missing!"
|
|
1283
|
+
)
|
|
1284
|
+
else:
|
|
1285
|
+
logger.error(
|
|
1286
|
+
f"❌ Casbin provider initialization returned None for '{slug}' - "
|
|
1287
|
+
f"check logs above for errors"
|
|
1288
|
+
)
|
|
1289
|
+
logger.error(f"❌ This means authorization will NOT work for '{slug}'")
|
|
1290
|
+
except ImportError as e:
|
|
1291
|
+
# ImportError is expected if Casbin is not installed
|
|
1292
|
+
logger.warning(
|
|
1293
|
+
f"❌ Casbin not available for '{slug}': {e}. "
|
|
1294
|
+
"Install with: pip install mdb-engine[casbin]"
|
|
1295
|
+
)
|
|
1296
|
+
except (ValueError, TypeError, RuntimeError, AttributeError, KeyError) as e:
|
|
1297
|
+
logger.exception(f"❌ Failed to initialize Casbin provider for '{slug}': {e}")
|
|
1298
|
+
# Informational message, not exception logging
|
|
1299
|
+
logger.error( # noqa: TRY400
|
|
1300
|
+
f"❌ This means authorization will NOT work for '{slug}' - "
|
|
1301
|
+
f"app.state.authz_provider will remain None"
|
|
1302
|
+
)
|
|
1303
|
+
except (
|
|
1304
|
+
RuntimeError,
|
|
1305
|
+
ValueError,
|
|
1306
|
+
AttributeError,
|
|
1307
|
+
TypeError,
|
|
1308
|
+
ConnectionError,
|
|
1309
|
+
OSError,
|
|
1310
|
+
) as e:
|
|
1311
|
+
# Catch specific exceptions that might occur during initialization
|
|
1312
|
+
logger.exception(
|
|
1313
|
+
f"❌ Unexpected error initializing Casbin provider for '{slug}': {e}"
|
|
1314
|
+
)
|
|
1315
|
+
# Informational message, not exception logging
|
|
1316
|
+
logger.error( # noqa: TRY400
|
|
1317
|
+
f"❌ This means authorization will NOT work for '{slug}' - "
|
|
1318
|
+
f"app.state.authz_provider will remain None"
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
elif authz_provider_type is None and auth_policy:
|
|
1322
|
+
# Default to Casbin if provider not specified but auth.policy exists
|
|
1323
|
+
logger.info(
|
|
1324
|
+
f"⚠️ No provider specified in auth.policy for '{slug}', "
|
|
1325
|
+
f"defaulting to Casbin"
|
|
1326
|
+
)
|
|
1327
|
+
try:
|
|
1328
|
+
from ..auth.casbin_factory import initialize_casbin_from_manifest
|
|
1329
|
+
|
|
1330
|
+
authz_provider = await initialize_casbin_from_manifest(
|
|
1331
|
+
engine, slug, app_manifest
|
|
1332
|
+
)
|
|
1333
|
+
if authz_provider:
|
|
1334
|
+
app.state.authz_provider = authz_provider
|
|
1335
|
+
logger.info(f"✅ Casbin provider auto-initialized for '{slug}' (default)")
|
|
1336
|
+
else:
|
|
1337
|
+
logger.warning(
|
|
1338
|
+
f"⚠️ Casbin provider not initialized for '{slug}' "
|
|
1339
|
+
f"(default attempt failed)"
|
|
1340
|
+
)
|
|
1341
|
+
except ImportError as e:
|
|
1342
|
+
logger.warning(
|
|
1343
|
+
f"⚠️ Casbin not available for '{slug}': {e}. "
|
|
1344
|
+
"Install with: pip install mdb-engine[casbin]"
|
|
1345
|
+
)
|
|
1346
|
+
except (
|
|
1347
|
+
ValueError,
|
|
1348
|
+
TypeError,
|
|
1349
|
+
RuntimeError,
|
|
1350
|
+
AttributeError,
|
|
1351
|
+
KeyError,
|
|
1352
|
+
) as e:
|
|
1353
|
+
logger.exception(
|
|
1354
|
+
f"❌ Failed to initialize Casbin provider for '{slug}' (default): {e}"
|
|
1355
|
+
)
|
|
1356
|
+
elif authz_provider_type:
|
|
1357
|
+
logger.warning(
|
|
1358
|
+
f"⚠️ Unknown authz provider type '{authz_provider_type}' for '{slug}' - "
|
|
1359
|
+
f"skipping initialization"
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
# Auto-seed demo users if configured in manifest
|
|
1363
|
+
users_config = auth_config.get("users", {})
|
|
1364
|
+
if users_config.get("enabled") and users_config.get("demo_users"):
|
|
1365
|
+
try:
|
|
1366
|
+
from ..auth import ensure_demo_users_exist
|
|
1367
|
+
|
|
1368
|
+
db = engine.get_scoped_db(slug)
|
|
1369
|
+
demo_users = await ensure_demo_users_exist(
|
|
1370
|
+
db=db,
|
|
1371
|
+
slug_id=slug,
|
|
1372
|
+
config=app_manifest,
|
|
1373
|
+
)
|
|
1374
|
+
if demo_users:
|
|
1375
|
+
logger.info(f"✅ Seeded {len(demo_users)} demo user(s) for '{slug}'")
|
|
1376
|
+
except (
|
|
1377
|
+
ImportError,
|
|
1378
|
+
ValueError,
|
|
1379
|
+
TypeError,
|
|
1380
|
+
RuntimeError,
|
|
1381
|
+
AttributeError,
|
|
1382
|
+
KeyError,
|
|
1383
|
+
) as e:
|
|
1384
|
+
logger.warning(f"⚠️ Failed to seed demo users for '{slug}': {e}")
|
|
1385
|
+
|
|
1160
1386
|
# Expose engine state on app.state
|
|
1161
1387
|
app.state.engine = engine
|
|
1162
1388
|
app.state.app_slug = slug
|
|
@@ -1165,13 +1391,57 @@ class MongoDBEngine:
|
|
|
1165
1391
|
app.state.auth_mode = auth_mode
|
|
1166
1392
|
app.state.ray_actor = engine.ray_actor
|
|
1167
1393
|
|
|
1394
|
+
# Initialize DI container (if not already set)
|
|
1395
|
+
from ..di import Container
|
|
1396
|
+
|
|
1397
|
+
if not hasattr(app.state, "container") or app.state.container is None:
|
|
1398
|
+
app.state.container = Container()
|
|
1399
|
+
logger.debug(f"DI Container initialized for '{slug}'")
|
|
1400
|
+
|
|
1401
|
+
# Call on_startup callback if provided
|
|
1402
|
+
if on_startup:
|
|
1403
|
+
try:
|
|
1404
|
+
await on_startup(app, engine, app_manifest)
|
|
1405
|
+
logger.info(f"on_startup callback completed for '{slug}'")
|
|
1406
|
+
except (ValueError, TypeError, RuntimeError, AttributeError, KeyError) as e:
|
|
1407
|
+
logger.exception(f"on_startup callback failed for '{slug}': {e}")
|
|
1408
|
+
raise
|
|
1409
|
+
|
|
1168
1410
|
yield
|
|
1169
1411
|
|
|
1412
|
+
# Call on_shutdown callback if provided
|
|
1413
|
+
if on_shutdown:
|
|
1414
|
+
try:
|
|
1415
|
+
await on_shutdown(app, engine, app_manifest)
|
|
1416
|
+
logger.info(f"on_shutdown callback completed for '{slug}'")
|
|
1417
|
+
except (ValueError, TypeError, RuntimeError, AttributeError, KeyError) as e:
|
|
1418
|
+
logger.warning(f"on_shutdown callback failed for '{slug}': {e}")
|
|
1419
|
+
|
|
1170
1420
|
await engine.shutdown()
|
|
1171
1421
|
|
|
1172
1422
|
# Create FastAPI app
|
|
1173
1423
|
app = FastAPI(title=app_title, lifespan=lifespan, **fastapi_kwargs)
|
|
1174
1424
|
|
|
1425
|
+
# Add request scope middleware (innermost layer - runs first on request)
|
|
1426
|
+
# This sets up the DI request scope for each request
|
|
1427
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
1428
|
+
|
|
1429
|
+
from ..di import ScopeManager
|
|
1430
|
+
|
|
1431
|
+
class RequestScopeMiddleware(BaseHTTPMiddleware):
|
|
1432
|
+
"""Middleware that manages request-scoped DI instances."""
|
|
1433
|
+
|
|
1434
|
+
async def dispatch(self, request, call_next):
|
|
1435
|
+
ScopeManager.begin_request()
|
|
1436
|
+
try:
|
|
1437
|
+
response = await call_next(request)
|
|
1438
|
+
return response
|
|
1439
|
+
finally:
|
|
1440
|
+
ScopeManager.end_request()
|
|
1441
|
+
|
|
1442
|
+
app.add_middleware(RequestScopeMiddleware)
|
|
1443
|
+
logger.debug(f"RequestScopeMiddleware added for '{slug}'")
|
|
1444
|
+
|
|
1175
1445
|
# Add rate limiting middleware FIRST (outermost layer)
|
|
1176
1446
|
# This ensures rate limiting happens before auth validation
|
|
1177
1447
|
rate_limits_config = auth_config.get("rate_limits", {})
|
|
@@ -1236,6 +1506,7 @@ class MongoDBEngine:
|
|
|
1236
1506
|
async def _initialize_shared_user_pool(
|
|
1237
1507
|
self,
|
|
1238
1508
|
app: "FastAPI",
|
|
1509
|
+
manifest: Optional[Dict[str, Any]] = None,
|
|
1239
1510
|
) -> None:
|
|
1240
1511
|
"""
|
|
1241
1512
|
Initialize shared user pool, audit log, and set them on app.state.
|
|
@@ -1251,6 +1522,7 @@ class MongoDBEngine:
|
|
|
1251
1522
|
|
|
1252
1523
|
Args:
|
|
1253
1524
|
app: FastAPI application instance
|
|
1525
|
+
manifest: Optional manifest dict for seeding demo users
|
|
1254
1526
|
"""
|
|
1255
1527
|
from ..auth.audit import AuthAuditLog
|
|
1256
1528
|
from ..auth.shared_users import SharedUserPool
|
|
@@ -1275,8 +1547,37 @@ class MongoDBEngine:
|
|
|
1275
1547
|
# Expose user pool on app.state for middleware to access
|
|
1276
1548
|
app.state.user_pool = self._shared_user_pool
|
|
1277
1549
|
|
|
1550
|
+
# Seed demo users to SharedUserPool if configured in manifest
|
|
1551
|
+
if manifest:
|
|
1552
|
+
auth_config = manifest.get("auth", {})
|
|
1553
|
+
users_config = auth_config.get("users", {})
|
|
1554
|
+
demo_users = users_config.get("demo_users", [])
|
|
1555
|
+
|
|
1556
|
+
if demo_users and users_config.get("demo_user_seed_strategy", "auto") != "disabled":
|
|
1557
|
+
for demo in demo_users:
|
|
1558
|
+
try:
|
|
1559
|
+
email = demo.get("email")
|
|
1560
|
+
password = demo.get("password")
|
|
1561
|
+
app_roles = demo.get("app_roles", {})
|
|
1562
|
+
|
|
1563
|
+
existing = await self._shared_user_pool.get_user_by_email(email)
|
|
1564
|
+
|
|
1565
|
+
if not existing:
|
|
1566
|
+
await self._shared_user_pool.create_user(
|
|
1567
|
+
email=email,
|
|
1568
|
+
password=password,
|
|
1569
|
+
app_roles=app_roles,
|
|
1570
|
+
)
|
|
1571
|
+
logger.info(f"✅ Created shared demo user: {email}")
|
|
1572
|
+
else:
|
|
1573
|
+
logger.debug(f"ℹ️ Shared demo user exists: {email}")
|
|
1574
|
+
except (ValueError, TypeError, RuntimeError, AttributeError, KeyError) as e:
|
|
1575
|
+
logger.warning(
|
|
1576
|
+
f"⚠️ Failed to create shared demo user {demo.get('email')}: {e}"
|
|
1577
|
+
)
|
|
1578
|
+
|
|
1278
1579
|
# Initialize audit logging if enabled
|
|
1279
|
-
auth_config =
|
|
1580
|
+
auth_config = (manifest or {}).get("auth", {})
|
|
1280
1581
|
audit_config = auth_config.get("audit", {})
|
|
1281
1582
|
audit_enabled = audit_config.get("enabled", True) # Default: enabled for shared auth
|
|
1282
1583
|
|
mdb_engine/core/manifest.py
CHANGED
|
@@ -300,24 +300,44 @@ MANIFEST_SCHEMA_V2 = {
|
|
|
300
300
|
"initial_policies": {
|
|
301
301
|
"type": "array",
|
|
302
302
|
"items": {
|
|
303
|
-
"
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
"
|
|
308
|
-
"
|
|
303
|
+
"oneOf": [
|
|
304
|
+
{
|
|
305
|
+
"type": "array",
|
|
306
|
+
"items": {"type": "string"},
|
|
307
|
+
"minItems": 3,
|
|
308
|
+
"maxItems": 3,
|
|
309
|
+
"description": (
|
|
310
|
+
"Casbin policy as array: "
|
|
311
|
+
'["role", "resource", "action"]'
|
|
312
|
+
),
|
|
309
313
|
},
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
+
{
|
|
315
|
+
"type": "object",
|
|
316
|
+
"properties": {
|
|
317
|
+
"role": {"type": "string"},
|
|
318
|
+
"resource": {
|
|
319
|
+
"type": "string",
|
|
320
|
+
"default": "documents",
|
|
321
|
+
},
|
|
322
|
+
"action": {"type": "string"},
|
|
323
|
+
},
|
|
324
|
+
"required": ["role", "action"],
|
|
325
|
+
"additionalProperties": False,
|
|
326
|
+
"description": (
|
|
327
|
+
"OSO policy as object: "
|
|
328
|
+
'{"role": "admin", "resource": "documents", '
|
|
329
|
+
'"action": "read"}'
|
|
330
|
+
),
|
|
331
|
+
},
|
|
332
|
+
],
|
|
314
333
|
},
|
|
315
334
|
"description": (
|
|
316
|
-
"Initial permission policies to set up "
|
|
317
|
-
"
|
|
318
|
-
"
|
|
319
|
-
|
|
320
|
-
'"
|
|
335
|
+
"Initial permission policies to set up on startup. "
|
|
336
|
+
"For Casbin provider: use arrays like "
|
|
337
|
+
'["admin", "clicks", "read"]. '
|
|
338
|
+
"For OSO Cloud provider: use objects like "
|
|
339
|
+
'{"role": "admin", "resource": "documents", '
|
|
340
|
+
'"action": "read"}.'
|
|
321
341
|
),
|
|
322
342
|
},
|
|
323
343
|
},
|
mdb_engine/database/README.md
CHANGED
|
@@ -576,7 +576,34 @@ except (ConnectionFailure, ServerSelectionTimeoutError) as e:
|
|
|
576
576
|
|
|
577
577
|
## Integration Examples
|
|
578
578
|
|
|
579
|
-
### FastAPI Integration
|
|
579
|
+
### FastAPI Integration (Recommended)
|
|
580
|
+
|
|
581
|
+
Use the request-scoped `get_scoped_db` dependency from `mdb_engine.dependencies`:
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
from fastapi import Depends
|
|
585
|
+
from mdb_engine import MongoDBEngine
|
|
586
|
+
from mdb_engine.dependencies import get_scoped_db
|
|
587
|
+
|
|
588
|
+
engine = MongoDBEngine(mongo_uri="...", db_name="...")
|
|
589
|
+
app = engine.create_app(slug="my_app", manifest=Path("manifest.json"))
|
|
590
|
+
|
|
591
|
+
@app.get("/data")
|
|
592
|
+
async def get_data(db=Depends(get_scoped_db)):
|
|
593
|
+
# db is automatically scoped to "my_app"
|
|
594
|
+
docs = await db.my_collection.find({}).to_list(length=10)
|
|
595
|
+
return {"data": docs}
|
|
596
|
+
|
|
597
|
+
@app.post("/data")
|
|
598
|
+
async def create_data(db=Depends(get_scoped_db)):
|
|
599
|
+
# Writes are automatically scoped to "my_app"
|
|
600
|
+
result = await db.my_collection.insert_one({"name": "New Document"})
|
|
601
|
+
return {"inserted_id": str(result.inserted_id)}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Legacy FastAPI Integration
|
|
605
|
+
|
|
606
|
+
For apps not using `engine.create_app()`:
|
|
580
607
|
|
|
581
608
|
```python
|
|
582
609
|
from fastapi import FastAPI, Depends
|