mdb-engine 0.1.7__py3-none-any.whl → 0.2.1__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.
@@ -16,14 +16,14 @@ from typing import TYPE_CHECKING, Any, Optional
16
16
  from .casbin_models import DEFAULT_RBAC_MODEL, SIMPLE_ACL_MODEL
17
17
 
18
18
  if TYPE_CHECKING:
19
- import casbin
19
+ import casbin # type: ignore
20
20
 
21
21
  from .provider import CasbinAdapter
22
22
 
23
23
  logger = logging.getLogger(__name__)
24
24
 
25
25
 
26
- def get_casbin_model(model_type: str = "rbac") -> str:
26
+ async def get_casbin_model(model_type: str = "rbac") -> str:
27
27
  """
28
28
  Get Casbin model string by type or path.
29
29
 
@@ -39,16 +39,32 @@ def get_casbin_model(model_type: str = "rbac") -> str:
39
39
  return SIMPLE_ACL_MODEL
40
40
  else:
41
41
  # Assume it's a file path
42
+ # Try async file reading first, fallback to sync if not available
42
43
  model_path = Path(model_type)
43
44
  if model_path.exists():
44
- return model_path.read_text()
45
+ try:
46
+ # Try async file reading (non-blocking)
47
+ import aiofiles
48
+
49
+ async with aiofiles.open(model_path, "r") as f:
50
+ content = await f.read()
51
+ logger.debug(f"Read model file asynchronously: {model_path}")
52
+ return content
53
+ except ImportError:
54
+ # Fallback to sync read if aiofiles not available
55
+ # This is acceptable during startup initialization
56
+ logger.debug(
57
+ "aiofiles not available, using sync file read (acceptable during startup)"
58
+ )
59
+ return model_path.read_text()
45
60
  else:
46
61
  logger.warning(f"Casbin model file not found: {model_type}, using default RBAC model")
47
62
  return DEFAULT_RBAC_MODEL
48
63
 
49
64
 
50
65
  async def create_casbin_enforcer(
51
- db,
66
+ mongo_uri: str,
67
+ db_name: str,
52
68
  model: str = "rbac",
53
69
  policies_collection: str = "casbin_policies",
54
70
  default_roles: Optional[list] = None,
@@ -57,7 +73,8 @@ async def create_casbin_enforcer(
57
73
  Create a Casbin AsyncEnforcer with MongoDB adapter.
58
74
 
59
75
  Args:
60
- db: Scoped MongoDB database instance (ScopedMongoWrapper)
76
+ mongo_uri: MongoDB connection URI string
77
+ db_name: MongoDB database name
61
78
  model: Casbin model type ("rbac", "acl") or path to model file
62
79
  policies_collection: MongoDB collection name for policies (will be app-scoped)
63
80
  default_roles: List of default roles to create (optional)
@@ -69,30 +86,94 @@ async def create_casbin_enforcer(
69
86
  ImportError: If casbin or casbin-motor-adapter is not installed
70
87
  """
71
88
  try:
72
- import casbin
73
- from casbin_motor_adapter import MotorAdapter
89
+ import casbin # type: ignore
90
+ from casbin_motor_adapter import Adapter # type: ignore
74
91
  except ImportError as e:
75
92
  raise ImportError(
76
93
  "Casbin dependencies not installed. Install with: pip install mdb-engine[casbin]"
77
94
  ) from e
78
95
 
79
- # Get model string
80
- model_str = get_casbin_model(model)
96
+ # Get model string (async)
97
+ model_str = await get_casbin_model(model)
81
98
 
82
99
  # Create MongoDB adapter
83
- adapter = MotorAdapter(db, policies_collection)
100
+ # Try to pass policies_collection if supported by the adapter
101
+ logger.debug(
102
+ f"Creating Casbin MotorAdapter with URI: {mongo_uri[:50]}..., "
103
+ f"db_name: {db_name}, collection: {policies_collection}"
104
+ )
105
+ try:
106
+ # Try passing collection name as third parameter
107
+ try:
108
+ adapter = Adapter(mongo_uri, db_name, policies_collection)
109
+ logger.debug(
110
+ f"Casbin MotorAdapter created successfully with custom "
111
+ f"collection '{policies_collection}'"
112
+ )
113
+ except TypeError:
114
+ # Fallback: adapter doesn't support collection parameter, use default
115
+ logger.warning(
116
+ "Adapter doesn't support custom collection name, " "using default collection"
117
+ )
118
+ adapter = Adapter(mongo_uri, db_name)
119
+ logger.debug("Casbin MotorAdapter created successfully (using default collection)")
120
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError):
121
+ logger.exception("Failed to create Casbin MotorAdapter")
122
+ raise
84
123
 
85
124
  # Create enforcer with model and adapter
86
- enforcer = casbin.AsyncEnforcer()
87
- await enforcer.set_model(casbin.new_model_from_string(model_str))
88
- enforcer.set_adapter(adapter)
89
-
90
- # Load policies from database
91
- await enforcer.load_policy()
92
-
93
- # Create default roles if specified
94
- if default_roles:
95
- await _create_default_roles(enforcer, default_roles)
125
+ # AsyncEnforcer can accept model string or Model object
126
+ logger.debug("Creating Casbin AsyncEnforcer with model and adapter...")
127
+ try:
128
+ logger.debug(f"Model string length: {len(model_str)} chars")
129
+ logger.debug(f"Adapter type: {type(adapter)}")
130
+
131
+ # Create Model object from string
132
+ model = casbin.Model()
133
+ model.load_model_from_text(model_str)
134
+ logger.debug(f"Model object created successfully, type: {type(model)}")
135
+
136
+ # Create enforcer with Model object and adapter
137
+ # AsyncEnforcer auto-loads by default (auto_load=True), so we don't need manual load
138
+ logger.debug("Calling casbin.AsyncEnforcer(model, adapter)...")
139
+ enforcer = casbin.AsyncEnforcer(model, adapter)
140
+ logger.debug("Enforcer created successfully")
141
+
142
+ # Check if policies were auto-loaded
143
+ # If auto_load is enabled (default), policies are already loaded
144
+ # Only manually load if auto_load was disabled
145
+ if not getattr(enforcer, "auto_load", True):
146
+ logger.debug("Auto-load disabled, manually loading policies...")
147
+ await enforcer.load_policy()
148
+ logger.debug("Policies loaded successfully")
149
+ else:
150
+ logger.debug("Policies auto-loaded by AsyncEnforcer constructor")
151
+ except (RuntimeError, ValueError, AttributeError, TypeError, OSError):
152
+ logger.exception("Failed to create or configure Casbin enforcer")
153
+ # Try alternative: use temp file approach
154
+ logger.info("Attempting alternative: using temporary model file...")
155
+ try:
156
+ import os
157
+ import tempfile
158
+
159
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".conf", delete=False) as f:
160
+ f.write(model_str)
161
+ temp_model_path = f.name
162
+ try:
163
+ enforcer = casbin.AsyncEnforcer(temp_model_path, adapter)
164
+ logger.info("✅ Enforcer created successfully using temp model file")
165
+ # Clean up temp file
166
+ os.unlink(temp_model_path)
167
+ except (RuntimeError, ValueError, AttributeError, TypeError, OSError) as e2:
168
+ if os.path.exists(temp_model_path):
169
+ os.unlink(temp_model_path)
170
+ logger.exception("Alternative approach also failed")
171
+ raise RuntimeError("Failed to create Casbin enforcer") from e2
172
+ except (OSError, RuntimeError) as e2:
173
+ logger.exception("Failed to try alternative approach")
174
+ raise RuntimeError("Failed to create Casbin enforcer") from e2
175
+
176
+ # Note: Removed default_roles creation - roles exist implicitly when assigned to users
96
177
 
97
178
  logger.info(
98
179
  f"Casbin enforcer created with model '{model}' and "
@@ -102,28 +183,8 @@ async def create_casbin_enforcer(
102
183
  return enforcer
103
184
 
104
185
 
105
- async def _create_default_roles(enforcer: casbin.AsyncEnforcer, roles: list) -> None:
106
- """
107
- Create default roles in Casbin (as grouping rules).
108
-
109
- Args:
110
- enforcer: Casbin AsyncEnforcer instance
111
- roles: List of role names to create
112
- """
113
- for role in roles:
114
- # Create a grouping rule: role -> role (self-reference for role existence)
115
- # This ensures the role exists in the system
116
- # Actual user-role assignments will be added when users are created
117
- try:
118
- # Check if role already exists
119
- existing = await enforcer.get_roles_for_user(role)
120
- if not existing:
121
- # Add role as a self-grouping rule to ensure it exists
122
- # This is a common pattern to "register" roles
123
- await enforcer.add_grouping_policy(role, role)
124
- logger.debug(f"Created default Casbin role: {role}")
125
- except (AttributeError, TypeError, ValueError, RuntimeError) as e:
126
- logger.warning(f"Error creating default role '{role}': {e}")
186
+ # Removed _create_default_roles function - roles exist implicitly when assigned to users
187
+ # No need to "create" roles beforehand with self-referencing policies
127
188
 
128
189
 
129
190
  async def initialize_casbin_from_manifest(
@@ -135,7 +196,7 @@ async def initialize_casbin_from_manifest(
135
196
  Args:
136
197
  engine: MongoDBEngine instance
137
198
  app_slug: App slug identifier
138
- auth_config: Auth configuration dict from manifest (contains auth_policy)
199
+ auth_config: Full manifest config dict (contains auth.policy or auth_policy)
139
200
 
140
201
  Returns:
141
202
  CasbinAdapter instance if successfully created, None otherwise
@@ -143,40 +204,176 @@ async def initialize_casbin_from_manifest(
143
204
  try:
144
205
  from .provider import CasbinAdapter
145
206
 
146
- auth_policy = auth_config.get("auth_policy", {})
207
+ # Support both old (auth_policy) and new (auth.policy) structures
208
+ auth = auth_config.get("auth", {})
209
+ auth_policy = auth.get("policy", {}) or auth_config.get("auth_policy", {})
147
210
  provider = auth_policy.get("provider", "casbin")
148
211
 
149
212
  # Only proceed if provider is casbin
150
213
  if provider != "casbin":
214
+ logger.debug(f"Provider is '{provider}', not 'casbin' - skipping Casbin initialization")
151
215
  return None
152
216
 
217
+ logger.info(f"Initializing Casbin provider for app '{app_slug}'...")
218
+
153
219
  # Get authorization config
154
220
  authorization = auth_policy.get("authorization", {})
155
221
  model = authorization.get("model", "rbac")
156
222
  policies_collection = authorization.get("policies_collection", "casbin_policies")
157
223
  default_roles = authorization.get("default_roles", [])
224
+ initial_policies = authorization.get("initial_policies", [])
225
+ initial_roles = authorization.get("initial_roles", [])
158
226
 
159
- # Get scoped database from engine
160
- db = engine.get_scoped_db(app_slug)
161
-
162
- # Create enforcer
163
- enforcer = await create_casbin_enforcer(
164
- db=db,
165
- model=model,
166
- policies_collection=policies_collection,
167
- default_roles=default_roles,
227
+ # Create enforcer with MongoDB connection info from engine
228
+ logger.debug(
229
+ f"Creating Casbin enforcer with URI: {engine.mongo_uri[:50]}..., "
230
+ f"db: {engine.db_name}"
168
231
  )
232
+ try:
233
+ enforcer = await create_casbin_enforcer(
234
+ mongo_uri=engine.mongo_uri,
235
+ db_name=engine.db_name,
236
+ model=model,
237
+ policies_collection=policies_collection,
238
+ default_roles=default_roles,
239
+ )
240
+ logger.debug("Casbin enforcer created successfully")
241
+ except (
242
+ RuntimeError,
243
+ ValueError,
244
+ AttributeError,
245
+ TypeError,
246
+ ConnectionError,
247
+ ImportError,
248
+ ):
249
+ logger.exception(f"Failed to create Casbin enforcer for '{app_slug}'")
250
+ return None
169
251
 
170
252
  # Create adapter
171
- adapter = CasbinAdapter(enforcer)
253
+ try:
254
+ adapter = CasbinAdapter(enforcer)
255
+ logger.info("✅ CasbinAdapter created successfully")
256
+ except (RuntimeError, ValueError, AttributeError, TypeError):
257
+ logger.exception(f"Failed to create CasbinAdapter for '{app_slug}'")
258
+ return None
259
+
260
+ # Set up initial policies if configured
261
+ if initial_policies:
262
+ logger.info(f"Setting up {len(initial_policies)} initial policies...")
263
+ for policy in initial_policies:
264
+ if isinstance(policy, (list, tuple)) and len(policy) >= 3:
265
+ role, resource, action = policy[0], policy[1], policy[2]
266
+ try:
267
+ # Check if policy already exists
268
+ exists = await adapter.has_policy(role, resource, action)
269
+ if exists:
270
+ logger.debug(f" Policy already exists: {role} -> {resource}:{action}")
271
+ else:
272
+ added = await adapter.add_policy(role, resource, action)
273
+ if added:
274
+ logger.debug(f" Added policy: {role} -> {resource}:{action}")
275
+ else:
276
+ logger.warning(
277
+ f" Failed to add policy: {role} -> " f"{resource}:{action}"
278
+ )
279
+ except (ValueError, TypeError, RuntimeError, AttributeError) as e:
280
+ logger.warning(f" Failed to add policy {policy}: {e}", exc_info=True)
281
+
282
+ # Set up initial role assignments if configured
283
+ # Disable auto_save during bulk operations for better performance
284
+ if initial_roles:
285
+ logger.info(f"Setting up {len(initial_roles)} initial role assignments...")
286
+ # Temporarily disable auto_save to avoid writing on every iteration
287
+ original_auto_save = getattr(enforcer, "auto_save", True)
288
+ try:
289
+ enforcer.auto_save = False
290
+ logger.debug("Disabled auto_save for bulk role assignment")
291
+ except AttributeError:
292
+ # Some enforcer implementations don't support auto_save attribute
293
+ logger.debug("auto_save attribute not available, proceeding with default behavior")
294
+
295
+ for role_assignment in initial_roles:
296
+ if isinstance(role_assignment, dict):
297
+ user = role_assignment.get("user")
298
+ role = role_assignment.get("role")
299
+ if user and role:
300
+ try:
301
+ # Check if role assignment already exists
302
+ exists = await adapter.has_role_for_user(user, role)
303
+ logger.debug(
304
+ f" Checking role assignment: {user} -> {role}, " f"exists={exists}"
305
+ )
306
+ if exists:
307
+ logger.info(f" ✓ Role assignment already exists: {user} -> {role}")
308
+ else:
309
+ logger.info(f" Adding role assignment: {user} -> {role}")
310
+ # Use enforcer directly with add_grouping_policy
311
+ added = await enforcer.add_grouping_policy(user, role)
312
+ if added:
313
+ logger.info(
314
+ f" ✓ Successfully assigned role '{role}' "
315
+ f"to user '{user}'"
316
+ )
317
+ else:
318
+ logger.warning(
319
+ f" ⚠ Failed to assign role '{role}' to "
320
+ f"user '{user}' - add_grouping_policy returned False"
321
+ )
322
+ except (ValueError, TypeError, RuntimeError, AttributeError):
323
+ logger.exception(f" ✗ Exception assigning role {role_assignment}")
324
+
325
+ # Restore auto_save setting
326
+ if hasattr(enforcer, "auto_save"):
327
+ enforcer.auto_save = original_auto_save
328
+ logger.debug("Restored auto_save setting")
329
+
330
+ # Verify that policies and roles were set up correctly
331
+ if initial_policies:
332
+ verified = 0
333
+ for policy in initial_policies:
334
+ if isinstance(policy, (list, tuple)) and len(policy) >= 3:
335
+ role, resource, action = policy[0], policy[1], policy[2]
336
+ if await adapter.has_policy(role, resource, action):
337
+ verified += 1
338
+ logger.info(f"Verified {verified}/{len(initial_policies)} policies exist in memory")
339
+
340
+ if initial_roles:
341
+ verified = 0
342
+ for role_assignment in initial_roles:
343
+ if isinstance(role_assignment, dict):
344
+ user = role_assignment.get("user")
345
+ role = role_assignment.get("role")
346
+ if user and role and await adapter.has_role_for_user(user, role):
347
+ verified += 1
348
+ logger.info(
349
+ f"Verified {verified}/{len(initial_roles)} role assignments " f"exist in memory"
350
+ )
351
+
352
+ # Save policies to persist them to database
353
+ # Only save if auto_save was disabled (to avoid double-saving)
354
+ if not getattr(enforcer, "auto_save", True):
355
+ saved = await adapter.save_policy()
356
+ if saved:
357
+ logger.debug("Policies saved to database successfully")
358
+ else:
359
+ logger.warning(
360
+ "Failed to save policies to database - they may not " "persist across restarts"
361
+ )
362
+ else:
363
+ logger.debug(
364
+ "Skipping manual save_policy() - auto_save is enabled, "
365
+ "policies already persisted"
366
+ )
172
367
 
173
- logger.info(f"Casbin provider initialized for app '{app_slug}'")
368
+ logger.info(f"Casbin provider initialized for app '{app_slug}'")
369
+ logger.info(f"✅ CasbinAdapter ready for use - type: {type(adapter).__name__}")
174
370
 
175
371
  return adapter
176
372
 
177
373
  except ImportError as e:
374
+ # ImportError is expected if Casbin is not installed - use warning, not error
178
375
  logger.warning(
179
- f"Casbin not available for app '{app_slug}': {e}. "
376
+ f"Casbin not available for app '{app_slug}': {e}. "
180
377
  "Install with: pip install mdb-engine[casbin]"
181
378
  )
182
379
  return None
@@ -188,8 +385,10 @@ async def initialize_casbin_from_manifest(
188
385
  RuntimeError,
189
386
  KeyError,
190
387
  ) as e:
191
- logger.error(
192
- f"Error initializing Casbin provider for app '{app_slug}': {e}",
193
- exc_info=True,
388
+ logger.exception(f"❌ Error initializing Casbin provider for app '{app_slug}': {e}")
389
+ # Informational message, not exception logging
390
+ logger.error( # noqa: TRY400
391
+ f"❌ Casbin provider initialization FAILED for '{app_slug}' - "
392
+ "check logs above for detailed error information"
194
393
  )
195
394
  return None
@@ -41,15 +41,20 @@ def _get_secret_key() -> str:
41
41
  if _SECRET_KEY_CACHE is not None:
42
42
  return _SECRET_KEY_CACHE
43
43
 
44
- secret_key = os.environ.get("FLASK_SECRET_KEY") or os.environ.get("SECRET_KEY")
44
+ secret_key = (
45
+ os.environ.get("FLASK_SECRET_KEY")
46
+ or os.environ.get("SECRET_KEY")
47
+ or os.environ.get("APP_SECRET_KEY")
48
+ )
45
49
 
46
50
  if not secret_key:
47
51
  raise ConfigurationError(
48
- "FLASK_SECRET_KEY environment variable is required for JWT token security. "
49
- "Set a strong secret key (minimum 32 characters, cryptographically random). "
50
- "Example: export FLASK_SECRET_KEY=$(python -c "
52
+ "SECRET_KEY environment variable is required for JWT token security. "
53
+ "Set FLASK_SECRET_KEY, SECRET_KEY, or APP_SECRET_KEY with a strong secret key "
54
+ "(minimum 32 characters, cryptographically random). "
55
+ "Example: export SECRET_KEY=$(python -c "
51
56
  "'import secrets; print(secrets.token_urlsafe(32))')",
52
- config_key="FLASK_SECRET_KEY",
57
+ config_key="SECRET_KEY",
53
58
  )
54
59
 
55
60
  if len(secret_key) < 32:
@@ -273,14 +273,30 @@ async def _setup_demo_users(app: FastAPI, engine, slug_id: str, config: Dict[str
273
273
  if role_assignment.get("user") == user_email:
274
274
  role = role_assignment.get("role")
275
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
+
276
280
  try:
277
- await app.state.authz_provider.add_role_for_user(
278
- user_email, role, resource
279
- )
280
- logger.info(
281
- f"✅ Assigned role '{role}' on resource '{resource}' "
282
- f"to demo user '{user_email}' for {slug_id}"
283
- )
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
+ )
284
300
  except (
285
301
  ValueError,
286
302
  TypeError,
@@ -46,7 +46,7 @@ async def create_oso_cloud_client(
46
46
 
47
47
  # Import OSO Cloud SDK - the class is named "Oso"
48
48
  try:
49
- from oso_cloud import Oso
49
+ from oso_cloud import Oso # type: ignore
50
50
 
51
51
  logger.debug("✅ Imported Oso from oso_cloud")
52
52
  except ImportError as e:
@@ -212,7 +212,7 @@ async def initialize_oso_from_manifest(
212
212
  try:
213
213
  import asyncio
214
214
 
215
- from oso_cloud import Value
215
+ from oso_cloud import Value # type: ignore
216
216
 
217
217
  # Try a simple test authorization to verify connection
218
218
  test_actor = Value("User", "test")