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.
Files changed (87) hide show
  1. mdb_engine/__init__.py +104 -11
  2. mdb_engine/auth/ARCHITECTURE.md +112 -0
  3. mdb_engine/auth/README.md +648 -11
  4. mdb_engine/auth/__init__.py +136 -29
  5. mdb_engine/auth/audit.py +592 -0
  6. mdb_engine/auth/base.py +252 -0
  7. mdb_engine/auth/casbin_factory.py +264 -69
  8. mdb_engine/auth/config_helpers.py +7 -6
  9. mdb_engine/auth/cookie_utils.py +3 -7
  10. mdb_engine/auth/csrf.py +373 -0
  11. mdb_engine/auth/decorators.py +3 -10
  12. mdb_engine/auth/dependencies.py +47 -50
  13. mdb_engine/auth/helpers.py +3 -3
  14. mdb_engine/auth/integration.py +53 -80
  15. mdb_engine/auth/jwt.py +2 -6
  16. mdb_engine/auth/middleware.py +77 -34
  17. mdb_engine/auth/oso_factory.py +18 -38
  18. mdb_engine/auth/provider.py +270 -171
  19. mdb_engine/auth/rate_limiter.py +504 -0
  20. mdb_engine/auth/restrictions.py +8 -24
  21. mdb_engine/auth/session_manager.py +14 -29
  22. mdb_engine/auth/shared_middleware.py +600 -0
  23. mdb_engine/auth/shared_users.py +759 -0
  24. mdb_engine/auth/token_store.py +14 -28
  25. mdb_engine/auth/users.py +54 -113
  26. mdb_engine/auth/utils.py +213 -15
  27. mdb_engine/cli/commands/generate.py +545 -9
  28. mdb_engine/cli/commands/validate.py +3 -7
  29. mdb_engine/cli/utils.py +3 -3
  30. mdb_engine/config.py +7 -21
  31. mdb_engine/constants.py +65 -0
  32. mdb_engine/core/README.md +117 -6
  33. mdb_engine/core/__init__.py +39 -7
  34. mdb_engine/core/app_registration.py +22 -41
  35. mdb_engine/core/app_secrets.py +290 -0
  36. mdb_engine/core/connection.py +18 -9
  37. mdb_engine/core/encryption.py +223 -0
  38. mdb_engine/core/engine.py +1057 -93
  39. mdb_engine/core/index_management.py +12 -16
  40. mdb_engine/core/manifest.py +459 -150
  41. mdb_engine/core/ray_integration.py +435 -0
  42. mdb_engine/core/seeding.py +10 -18
  43. mdb_engine/core/service_initialization.py +12 -23
  44. mdb_engine/core/types.py +2 -5
  45. mdb_engine/database/README.md +140 -17
  46. mdb_engine/database/__init__.py +17 -6
  47. mdb_engine/database/abstraction.py +25 -37
  48. mdb_engine/database/connection.py +11 -18
  49. mdb_engine/database/query_validator.py +367 -0
  50. mdb_engine/database/resource_limiter.py +204 -0
  51. mdb_engine/database/scoped_wrapper.py +713 -196
  52. mdb_engine/dependencies.py +426 -0
  53. mdb_engine/di/__init__.py +34 -0
  54. mdb_engine/di/container.py +248 -0
  55. mdb_engine/di/providers.py +205 -0
  56. mdb_engine/di/scopes.py +139 -0
  57. mdb_engine/embeddings/README.md +54 -24
  58. mdb_engine/embeddings/__init__.py +31 -24
  59. mdb_engine/embeddings/dependencies.py +37 -154
  60. mdb_engine/embeddings/service.py +11 -25
  61. mdb_engine/exceptions.py +92 -0
  62. mdb_engine/indexes/README.md +30 -13
  63. mdb_engine/indexes/__init__.py +1 -0
  64. mdb_engine/indexes/helpers.py +1 -1
  65. mdb_engine/indexes/manager.py +50 -114
  66. mdb_engine/memory/README.md +2 -2
  67. mdb_engine/memory/__init__.py +1 -2
  68. mdb_engine/memory/service.py +30 -87
  69. mdb_engine/observability/README.md +4 -2
  70. mdb_engine/observability/__init__.py +26 -9
  71. mdb_engine/observability/health.py +8 -9
  72. mdb_engine/observability/metrics.py +32 -12
  73. mdb_engine/repositories/__init__.py +34 -0
  74. mdb_engine/repositories/base.py +325 -0
  75. mdb_engine/repositories/mongo.py +233 -0
  76. mdb_engine/repositories/unit_of_work.py +166 -0
  77. mdb_engine/routing/README.md +1 -1
  78. mdb_engine/routing/__init__.py +1 -3
  79. mdb_engine/routing/websockets.py +25 -60
  80. mdb_engine-0.2.0.dist-info/METADATA +313 -0
  81. mdb_engine-0.2.0.dist-info/RECORD +96 -0
  82. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  83. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  84. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
  85. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
  86. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
  87. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,252 @@
1
+ """
2
+ Authorization Engine Base Classes
3
+
4
+ Defines the abstract contract for authorization providers using the Adapter Pattern.
5
+ This ensures type safety, fail-closed security, and proper abstraction.
6
+
7
+ This module is part of MDB_ENGINE - MongoDB Engine.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import abc
13
+ import logging
14
+ from typing import Any, Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class AuthorizationError(Exception):
20
+ """
21
+ Base exception for authorization failures.
22
+
23
+ Ensures abstraction doesn't leak - application code doesn't need to know
24
+ if the failure came from Casbin, OSO, or any other engine.
25
+ """
26
+
27
+ pass
28
+
29
+
30
+ class BaseAuthorizationProvider(abc.ABC):
31
+ """
32
+ Abstract Base Class defining the contract for authorization providers.
33
+
34
+ Design Principles:
35
+ 1. Interface Segregation - Application only needs 'check', not engine internals
36
+ 2. Fail-Closed Security - Errors deny access, never grant it
37
+ 3. Adapter Pattern - Wraps third-party libraries without modifying them
38
+ 4. Type Safety - Clear contracts with proper type hints
39
+
40
+ This ABC ensures that all authorization providers:
41
+ - Have a consistent interface
42
+ - Fail securely (deny on error)
43
+ - Provide observability (structured logging)
44
+ - Handle edge cases gracefully
45
+ """
46
+
47
+ def __init__(self, engine_name: str):
48
+ """
49
+ Initialize the base provider.
50
+
51
+ Args:
52
+ engine_name: Human-readable name of the engine (e.g., "Casbin", "OSO Cloud")
53
+ """
54
+ self._engine_name = engine_name
55
+ self._initialized = False
56
+ logger.info(f"Initializing {engine_name} authorization provider")
57
+
58
+ @property
59
+ def engine_name(self) -> str:
60
+ """Get the name of the authorization engine."""
61
+ return self._engine_name
62
+
63
+ @property
64
+ def is_initialized(self) -> bool:
65
+ """Check if the provider has been properly initialized."""
66
+ return self._initialized
67
+
68
+ @abc.abstractmethod
69
+ async def check(
70
+ self,
71
+ subject: str,
72
+ resource: str,
73
+ action: str,
74
+ user_object: Optional[dict[str, Any]] = None,
75
+ ) -> bool:
76
+ """
77
+ Check if a subject is allowed to perform an action on a resource.
78
+
79
+ This is the primary authorization decision method. All implementations
80
+ must follow fail-closed security: if evaluation fails, deny access.
81
+
82
+ Args:
83
+ subject: Who is making the request (typically email or user ID)
84
+ resource: What resource they're accessing (e.g., "documents", "clicks")
85
+ action: What action they want to perform (e.g., "read", "write", "delete")
86
+ user_object: Optional user object with additional context
87
+
88
+ Returns:
89
+ True if authorized, False otherwise (including on error - fail-closed)
90
+
91
+ Raises:
92
+ AuthorizationError: Only for configuration/initialization errors,
93
+ not evaluation failures
94
+ """
95
+ pass
96
+
97
+ @abc.abstractmethod
98
+ async def add_policy(self, *params: Any) -> bool:
99
+ """
100
+ Add a policy rule to the authorization engine.
101
+
102
+ Args:
103
+ *params: Policy parameters (format depends on engine)
104
+
105
+ Returns:
106
+ True if policy was added successfully, False otherwise
107
+ """
108
+ pass
109
+
110
+ @abc.abstractmethod
111
+ async def add_role_for_user(self, *params: Any) -> bool:
112
+ """
113
+ Assign a role to a user.
114
+
115
+ Args:
116
+ *params: Role assignment parameters (format depends on engine)
117
+
118
+ Returns:
119
+ True if role was assigned successfully, False otherwise
120
+ """
121
+ pass
122
+
123
+ @abc.abstractmethod
124
+ async def save_policy(self) -> bool:
125
+ """
126
+ Persist policy changes to storage.
127
+
128
+ Returns:
129
+ True if saved successfully, False otherwise
130
+ """
131
+ pass
132
+
133
+ @abc.abstractmethod
134
+ async def has_policy(self, *params: Any) -> bool:
135
+ """
136
+ Check if a policy exists.
137
+
138
+ Args:
139
+ *params: Policy parameters to check
140
+
141
+ Returns:
142
+ True if policy exists, False otherwise
143
+ """
144
+ pass
145
+
146
+ @abc.abstractmethod
147
+ async def has_role_for_user(self, *params: Any) -> bool:
148
+ """
149
+ Check if a user has a specific role.
150
+
151
+ Args:
152
+ *params: User and role parameters
153
+
154
+ Returns:
155
+ True if user has the role, False otherwise
156
+ """
157
+ pass
158
+
159
+ @abc.abstractmethod
160
+ async def clear_cache(self) -> None:
161
+ """
162
+ Clear the authorization cache.
163
+
164
+ Should be called when policies or roles are modified to ensure
165
+ fresh authorization decisions.
166
+ """
167
+ pass
168
+
169
+ def _mark_initialized(self) -> None:
170
+ """Mark the provider as initialized (internal use only)."""
171
+ self._initialized = True
172
+ logger.info(f"✅ {self._engine_name} authorization provider initialized successfully")
173
+
174
+ def is_casbin(self) -> bool:
175
+ """
176
+ Check if this provider is a Casbin adapter.
177
+
178
+ Returns:
179
+ True if this is a CasbinAdapter, False otherwise
180
+ """
181
+ return hasattr(self, "_enforcer")
182
+
183
+ def is_oso(self) -> bool:
184
+ """
185
+ Check if this provider is an OSO adapter.
186
+
187
+ Returns:
188
+ True if this is an OsoAdapter, False otherwise
189
+ """
190
+ return hasattr(self, "_oso")
191
+
192
+ def _handle_evaluation_error(
193
+ self,
194
+ subject: str,
195
+ resource: str,
196
+ action: str,
197
+ error: Exception,
198
+ context: Optional[str] = None,
199
+ ) -> bool:
200
+ """
201
+ Handle authorization evaluation errors with fail-closed security.
202
+
203
+ Design Principle: Fail-Closed Security
204
+ - If the authorization engine crashes or errors, we MUST deny access
205
+ - Logging the error is critical for observability
206
+ - Never raise exceptions from evaluation errors (only from config errors)
207
+
208
+ Args:
209
+ subject: Subject that was being checked
210
+ resource: Resource that was being checked
211
+ action: Action that was being checked
212
+ error: The exception that occurred
213
+ context: Optional context string for logging
214
+
215
+ Returns:
216
+ False (deny access - fail-closed)
217
+ """
218
+ context_str = f" ({context})" if context else ""
219
+ logger.critical(
220
+ f"{self._engine_name} authorization evaluation failed{context_str}: "
221
+ f"subject={subject}, resource={resource}, action={action}, "
222
+ f"error={type(error).__name__}: {error}",
223
+ exc_info=True,
224
+ )
225
+ return False
226
+
227
+ def _handle_operation_error(
228
+ self,
229
+ operation: str,
230
+ error: Exception,
231
+ *params: Any,
232
+ ) -> bool:
233
+ """
234
+ Handle policy/role operation errors.
235
+
236
+ These are non-critical operations (adding policies, roles, etc.)
237
+ so we log warnings but don't fail-closed (return False).
238
+
239
+ Args:
240
+ operation: Name of the operation (e.g., "add_policy")
241
+ error: The exception that occurred
242
+ *params: Parameters that were passed to the operation
243
+
244
+ Returns:
245
+ False (operation failed)
246
+ """
247
+ logger.warning(
248
+ f"{self._engine_name} {operation} failed: "
249
+ f"params={params}, error={type(error).__name__}: {error}",
250
+ exc_info=True,
251
+ )
252
+ return False
@@ -11,19 +11,19 @@ from __future__ import annotations
11
11
 
12
12
  import logging
13
13
  from pathlib import Path
14
- from typing import TYPE_CHECKING, Any, Dict, Optional
14
+ from typing import TYPE_CHECKING, Any, Optional
15
15
 
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,18 +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
- logger.warning(
47
- f"Casbin model file not found: {model_type}, using default RBAC model"
48
- )
61
+ logger.warning(f"Casbin model file not found: {model_type}, using default RBAC model")
49
62
  return DEFAULT_RBAC_MODEL
50
63
 
51
64
 
52
65
  async def create_casbin_enforcer(
53
- db,
66
+ mongo_uri: str,
67
+ db_name: str,
54
68
  model: str = "rbac",
55
69
  policies_collection: str = "casbin_policies",
56
70
  default_roles: Optional[list] = None,
@@ -59,9 +73,10 @@ async def create_casbin_enforcer(
59
73
  Create a Casbin AsyncEnforcer with MongoDB adapter.
60
74
 
61
75
  Args:
62
- db: MongoDB database instance (Motor AsyncIOMotorDatabase)
76
+ mongo_uri: MongoDB connection URI string
77
+ db_name: MongoDB database name
63
78
  model: Casbin model type ("rbac", "acl") or path to model file
64
- policies_collection: MongoDB collection name for policies
79
+ policies_collection: MongoDB collection name for policies (will be app-scoped)
65
80
  default_roles: List of default roles to create (optional)
66
81
 
67
82
  Returns:
@@ -71,30 +86,94 @@ async def create_casbin_enforcer(
71
86
  ImportError: If casbin or casbin-motor-adapter is not installed
72
87
  """
73
88
  try:
74
- import casbin
75
- from casbin_motor_adapter import MotorAdapter
89
+ import casbin # type: ignore
90
+ from casbin_motor_adapter import Adapter # type: ignore
76
91
  except ImportError as e:
77
92
  raise ImportError(
78
93
  "Casbin dependencies not installed. Install with: pip install mdb-engine[casbin]"
79
94
  ) from e
80
95
 
81
- # Get model string
82
- model_str = get_casbin_model(model)
96
+ # Get model string (async)
97
+ model_str = await get_casbin_model(model)
83
98
 
84
99
  # Create MongoDB adapter
85
- 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
86
123
 
87
124
  # Create enforcer with model and adapter
88
- enforcer = casbin.AsyncEnforcer()
89
- await enforcer.set_model(casbin.new_model_from_string(model_str))
90
- enforcer.set_adapter(adapter)
91
-
92
- # Load policies from database
93
- await enforcer.load_policy()
94
-
95
- # Create default roles if specified
96
- if default_roles:
97
- 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
98
177
 
99
178
  logger.info(
100
179
  f"Casbin enforcer created with model '{model}' and "
@@ -104,40 +183,20 @@ async def create_casbin_enforcer(
104
183
  return enforcer
105
184
 
106
185
 
107
- async def _create_default_roles(enforcer: "casbin.AsyncEnforcer", roles: list) -> None:
108
- """
109
- Create default roles in Casbin (as grouping rules).
110
-
111
- Args:
112
- enforcer: Casbin AsyncEnforcer instance
113
- roles: List of role names to create
114
- """
115
- for role in roles:
116
- # Create a grouping rule: role -> role (self-reference for role existence)
117
- # This ensures the role exists in the system
118
- # Actual user-role assignments will be added when users are created
119
- try:
120
- # Check if role already exists
121
- existing = await enforcer.get_roles_for_user(role)
122
- if not existing:
123
- # Add role as a self-grouping rule to ensure it exists
124
- # This is a common pattern to "register" roles
125
- await enforcer.add_grouping_policy(role, role)
126
- logger.debug(f"Created default Casbin role: {role}")
127
- except (AttributeError, TypeError, ValueError, RuntimeError) as e:
128
- 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
129
188
 
130
189
 
131
190
  async def initialize_casbin_from_manifest(
132
- engine, app_slug: str, auth_config: Dict[str, Any]
133
- ) -> Optional["CasbinAdapter"]:
191
+ engine, app_slug: str, auth_config: dict[str, Any]
192
+ ) -> Optional[CasbinAdapter]:
134
193
  """
135
194
  Initialize Casbin provider from manifest configuration.
136
195
 
137
196
  Args:
138
197
  engine: MongoDBEngine instance
139
198
  app_slug: App slug identifier
140
- auth_config: Auth configuration dict from manifest (contains auth_policy)
199
+ auth_config: Full manifest config dict (contains auth.policy or auth_policy)
141
200
 
142
201
  Returns:
143
202
  CasbinAdapter instance if successfully created, None otherwise
@@ -145,42 +204,176 @@ async def initialize_casbin_from_manifest(
145
204
  try:
146
205
  from .provider import CasbinAdapter
147
206
 
148
- 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", {})
149
210
  provider = auth_policy.get("provider", "casbin")
150
211
 
151
212
  # Only proceed if provider is casbin
152
213
  if provider != "casbin":
214
+ logger.debug(f"Provider is '{provider}', not 'casbin' - skipping Casbin initialization")
153
215
  return None
154
216
 
217
+ logger.info(f"Initializing Casbin provider for app '{app_slug}'...")
218
+
155
219
  # Get authorization config
156
220
  authorization = auth_policy.get("authorization", {})
157
221
  model = authorization.get("model", "rbac")
158
- policies_collection = authorization.get(
159
- "policies_collection", "casbin_policies"
160
- )
222
+ policies_collection = authorization.get("policies_collection", "casbin_policies")
161
223
  default_roles = authorization.get("default_roles", [])
224
+ initial_policies = authorization.get("initial_policies", [])
225
+ initial_roles = authorization.get("initial_roles", [])
162
226
 
163
- # Get database from engine
164
- db = engine.get_database()
165
-
166
- # Create enforcer
167
- enforcer = await create_casbin_enforcer(
168
- db=db,
169
- model=model,
170
- policies_collection=policies_collection,
171
- 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}"
172
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
173
251
 
174
252
  # Create adapter
175
- 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
+ )
176
367
 
177
- 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__}")
178
370
 
179
371
  return adapter
180
372
 
181
373
  except ImportError as e:
374
+ # ImportError is expected if Casbin is not installed - use warning, not error
182
375
  logger.warning(
183
- f"Casbin not available for app '{app_slug}': {e}. "
376
+ f"Casbin not available for app '{app_slug}': {e}. "
184
377
  "Install with: pip install mdb-engine[casbin]"
185
378
  )
186
379
  return None
@@ -192,8 +385,10 @@ async def initialize_casbin_from_manifest(
192
385
  RuntimeError,
193
386
  KeyError,
194
387
  ) as e:
195
- logger.error(
196
- f"Error initializing Casbin provider for app '{app_slug}': {e}",
197
- 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"
198
393
  )
199
394
  return None