mdb-engine 0.1.6__py3-none-any.whl → 0.4.12__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 (92) hide show
  1. mdb_engine/__init__.py +116 -11
  2. mdb_engine/auth/ARCHITECTURE.md +112 -0
  3. mdb_engine/auth/README.md +654 -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 +265 -70
  8. mdb_engine/auth/config_defaults.py +5 -5
  9. mdb_engine/auth/config_helpers.py +19 -18
  10. mdb_engine/auth/cookie_utils.py +12 -16
  11. mdb_engine/auth/csrf.py +483 -0
  12. mdb_engine/auth/decorators.py +10 -16
  13. mdb_engine/auth/dependencies.py +69 -71
  14. mdb_engine/auth/helpers.py +3 -3
  15. mdb_engine/auth/integration.py +61 -88
  16. mdb_engine/auth/jwt.py +11 -15
  17. mdb_engine/auth/middleware.py +79 -35
  18. mdb_engine/auth/oso_factory.py +21 -41
  19. mdb_engine/auth/provider.py +270 -171
  20. mdb_engine/auth/rate_limiter.py +505 -0
  21. mdb_engine/auth/restrictions.py +21 -36
  22. mdb_engine/auth/session_manager.py +24 -41
  23. mdb_engine/auth/shared_middleware.py +977 -0
  24. mdb_engine/auth/shared_users.py +775 -0
  25. mdb_engine/auth/token_lifecycle.py +10 -12
  26. mdb_engine/auth/token_store.py +17 -32
  27. mdb_engine/auth/users.py +99 -159
  28. mdb_engine/auth/utils.py +236 -42
  29. mdb_engine/cli/commands/generate.py +546 -10
  30. mdb_engine/cli/commands/validate.py +3 -7
  31. mdb_engine/cli/utils.py +7 -7
  32. mdb_engine/config.py +13 -28
  33. mdb_engine/constants.py +65 -0
  34. mdb_engine/core/README.md +117 -6
  35. mdb_engine/core/__init__.py +39 -7
  36. mdb_engine/core/app_registration.py +31 -50
  37. mdb_engine/core/app_secrets.py +289 -0
  38. mdb_engine/core/connection.py +20 -12
  39. mdb_engine/core/encryption.py +222 -0
  40. mdb_engine/core/engine.py +2862 -115
  41. mdb_engine/core/index_management.py +12 -16
  42. mdb_engine/core/manifest.py +628 -204
  43. mdb_engine/core/ray_integration.py +436 -0
  44. mdb_engine/core/seeding.py +13 -21
  45. mdb_engine/core/service_initialization.py +20 -30
  46. mdb_engine/core/types.py +40 -43
  47. mdb_engine/database/README.md +140 -17
  48. mdb_engine/database/__init__.py +17 -6
  49. mdb_engine/database/abstraction.py +37 -50
  50. mdb_engine/database/connection.py +51 -30
  51. mdb_engine/database/query_validator.py +367 -0
  52. mdb_engine/database/resource_limiter.py +204 -0
  53. mdb_engine/database/scoped_wrapper.py +747 -237
  54. mdb_engine/dependencies.py +427 -0
  55. mdb_engine/di/__init__.py +34 -0
  56. mdb_engine/di/container.py +247 -0
  57. mdb_engine/di/providers.py +206 -0
  58. mdb_engine/di/scopes.py +139 -0
  59. mdb_engine/embeddings/README.md +54 -24
  60. mdb_engine/embeddings/__init__.py +31 -24
  61. mdb_engine/embeddings/dependencies.py +38 -155
  62. mdb_engine/embeddings/service.py +78 -75
  63. mdb_engine/exceptions.py +104 -12
  64. mdb_engine/indexes/README.md +30 -13
  65. mdb_engine/indexes/__init__.py +1 -0
  66. mdb_engine/indexes/helpers.py +11 -11
  67. mdb_engine/indexes/manager.py +59 -123
  68. mdb_engine/memory/README.md +95 -4
  69. mdb_engine/memory/__init__.py +1 -2
  70. mdb_engine/memory/service.py +363 -1168
  71. mdb_engine/observability/README.md +4 -2
  72. mdb_engine/observability/__init__.py +26 -9
  73. mdb_engine/observability/health.py +17 -17
  74. mdb_engine/observability/logging.py +10 -10
  75. mdb_engine/observability/metrics.py +40 -19
  76. mdb_engine/repositories/__init__.py +34 -0
  77. mdb_engine/repositories/base.py +325 -0
  78. mdb_engine/repositories/mongo.py +233 -0
  79. mdb_engine/repositories/unit_of_work.py +166 -0
  80. mdb_engine/routing/README.md +1 -1
  81. mdb_engine/routing/__init__.py +1 -3
  82. mdb_engine/routing/websockets.py +41 -75
  83. mdb_engine/utils/__init__.py +3 -1
  84. mdb_engine/utils/mongo.py +117 -0
  85. mdb_engine-0.4.12.dist-info/METADATA +492 -0
  86. mdb_engine-0.4.12.dist-info/RECORD +97 -0
  87. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
  88. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  89. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  90. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/entry_points.txt +0 -0
  91. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
  92. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
@@ -6,25 +6,31 @@ Defines the pluggable Authorization (AuthZ) interface for the platform.
6
6
  This module is part of MDB_ENGINE - MongoDB Engine.
7
7
  """
8
8
 
9
- from __future__ import \
10
- annotations # MUST be first import for string type hints
9
+ from __future__ import annotations # MUST be first import for string type hints
11
10
 
12
11
  import asyncio
13
12
  import logging
14
13
  import time
15
- from typing import TYPE_CHECKING, Any, Dict, Optional, Protocol, Tuple
14
+ from typing import TYPE_CHECKING, Any, Protocol
16
15
 
17
16
  from ..constants import AUTHZ_CACHE_TTL, MAX_CACHE_SIZE
18
17
 
18
+ # Import base class
19
+ from .base import AuthorizationError, BaseAuthorizationProvider
20
+
19
21
  if TYPE_CHECKING:
20
22
  import casbin
21
23
 
22
24
  logger = logging.getLogger(__name__)
23
25
 
24
26
 
27
+ # Keep Protocol for backward compatibility and type checking
25
28
  class AuthorizationProvider(Protocol):
26
29
  """
27
- Defines the "contract" for any pluggable authorization provider.
30
+ Protocol defining the "contract" for any pluggable authorization provider.
31
+
32
+ This Protocol is kept for backward compatibility and type checking.
33
+ All concrete implementations should extend BaseAuthorizationProvider instead.
28
34
  """
29
35
 
30
36
  async def check(
@@ -32,7 +38,7 @@ class AuthorizationProvider(Protocol):
32
38
  subject: str,
33
39
  resource: str,
34
40
  action: str,
35
- user_object: Optional[Dict[str, Any]] = None,
41
+ user_object: dict[str, Any] | None = None,
36
42
  ) -> bool:
37
43
  """
38
44
  Checks if a subject is allowed to perform an action on a resource.
@@ -40,36 +46,68 @@ class AuthorizationProvider(Protocol):
40
46
  ...
41
47
 
42
48
 
43
- class CasbinAdapter:
49
+ class CasbinAdapter(BaseAuthorizationProvider):
44
50
  """
45
- Implements the AuthorizationProvider interface using the Casbin AsyncEnforcer.
46
- Uses thread pool execution and caching to prevent blocking the event loop.
51
+ Adapter for Casbin authorization engine.
52
+
53
+ Implements the BaseAuthorizationProvider interface using Casbin AsyncEnforcer.
54
+ Uses the Adapter Pattern to wrap Casbin without modifying its source code.
55
+
56
+ Design Principles:
57
+ - Fail-Closed Security: Errors deny access
58
+ - Thread Pool Execution: Prevents blocking the event loop
59
+ - Caching: Improves performance for repeated checks
60
+ - Type Safety: Proper marshalling of Casbin's (sub, obj, act) format
47
61
  """
48
62
 
49
- # Use a string literal for the type hint to prevent module-level import
50
63
  def __init__(self, enforcer: casbin.AsyncEnforcer):
51
64
  """
52
- Initializes the adapter with a pre-configured Casbin AsyncEnforcer.
65
+ Initialize the Casbin adapter.
66
+
67
+ Args:
68
+ enforcer: Pre-configured Casbin AsyncEnforcer instance
69
+
70
+ Raises:
71
+ AuthorizationError: If Casbin is not available
53
72
  """
73
+ # Lazy import to allow code to exist without Casbin installed
74
+ # Import check is done via importlib in the factory, not here
75
+ try:
76
+ import casbin # noqa: F401
77
+ except ImportError as e:
78
+ raise AuthorizationError(
79
+ "Casbin library is not installed. " "Install with: pip install mdb-engine[casbin]"
80
+ ) from e
81
+
82
+ super().__init__(engine_name="Casbin")
54
83
  self._enforcer = enforcer
55
84
  # Cache for authorization results: {(subject, resource, action): (result, timestamp)}
56
- self._cache: Dict[Tuple[str, str, str], Tuple[bool, float]] = {}
85
+ self._cache: dict[tuple[str, str, str], tuple[bool, float]] = {}
57
86
  self._cache_lock = asyncio.Lock()
58
- logger.info(
59
- "✔️ CasbinAdapter initialized with async thread pool execution and caching."
60
- )
87
+ self._mark_initialized()
61
88
 
62
89
  async def check(
63
90
  self,
64
91
  subject: str,
65
92
  resource: str,
66
93
  action: str,
67
- user_object: Optional[Dict[str, Any]] = None,
94
+ user_object: dict[str, Any] | None = None,
68
95
  ) -> bool:
69
96
  """
70
- Performs the authorization check using the wrapped enforcer.
71
- Uses thread pool execution to prevent blocking the event loop and caches results.
97
+ Check authorization using Casbin's enforce method.
98
+
99
+ Implements fail-closed security: if evaluation fails, access is denied.
100
+ Uses thread pool execution to prevent blocking the event loop.
101
+
102
+ Casbin Format: enforce(subject, object, action)
103
+ - subject: Who is making the request (email or user ID)
104
+ - object: What resource they're accessing
105
+ - action: What action they want to perform
72
106
  """
107
+ if not self._initialized:
108
+ logger.error("CasbinAdapter not initialized - denying access")
109
+ return False
110
+
73
111
  cache_key = (subject, resource, action)
74
112
  current_time = time.time()
75
113
 
@@ -79,18 +117,20 @@ class CasbinAdapter:
79
117
  cached_result, cached_time = self._cache[cache_key]
80
118
  # Check if cache entry is still valid
81
119
  if current_time - cached_time < AUTHZ_CACHE_TTL:
82
- logger.debug(
83
- f"Authorization cache HIT for ({subject}, {resource}, {action})"
84
- )
120
+ logger.debug(f"Casbin cache HIT for ({subject}, {resource}, {action})")
85
121
  return cached_result
86
122
  # Cache expired, remove it
87
123
  del self._cache[cache_key]
88
124
 
89
125
  try:
90
- # The .enforce() method on AsyncEnforcer is synchronous and blocks the event loop.
126
+ # Casbin's enforce() is synchronous and blocks the event loop.
91
127
  # Run it in a thread pool to prevent blocking.
128
+ # Casbin order: (subject, object, action)
92
129
  result = await asyncio.to_thread(
93
- self._enforcer.enforce, subject, resource, action
130
+ self._enforcer.enforce,
131
+ subject, # Casbin subject
132
+ resource, # Casbin object
133
+ action, # Casbin action
94
134
  )
95
135
 
96
136
  # Cache the result
@@ -105,131 +145,198 @@ class CasbinAdapter:
105
145
  )[0]
106
146
  del self._cache[oldest_key]
107
147
 
148
+ logger.debug(f"Casbin authorization check: {subject} -> {resource}:{action} = {result}")
108
149
  return result
109
- except (AttributeError, TypeError, ValueError, RuntimeError) as e:
110
- logger.error(
111
- f"Casbin 'enforce' check failed for ({subject}, {resource}, {action}): {e}",
112
- exc_info=True,
113
- )
114
- return False
115
150
 
116
- async def clear_cache(self):
151
+ except (
152
+ RuntimeError,
153
+ ValueError,
154
+ AttributeError,
155
+ TypeError,
156
+ KeyError,
157
+ ConnectionError,
158
+ ) as e:
159
+ # Fail-Closed Security: Any exception denies access
160
+ # Catching specific exceptions from Casbin/enforce operations
161
+ return self._handle_evaluation_error(subject, resource, action, e, "enforce")
162
+
163
+ async def clear_cache(self) -> None:
117
164
  """
118
- Clears the authorization cache. Useful when policies are updated.
165
+ Clear the authorization cache.
166
+
167
+ Should be called when policies or roles are modified to ensure
168
+ fresh authorization decisions.
119
169
  """
120
170
  async with self._cache_lock:
121
171
  self._cache.clear()
122
- logger.info("Authorization cache cleared.")
172
+ logger.info(f"{self._engine_name} authorization cache cleared")
173
+
174
+ async def add_policy(self, *params: Any) -> bool:
175
+ """
176
+ Add a policy rule to Casbin.
123
177
 
124
- async def add_policy(self, *params) -> bool:
125
- """Helper to pass-through policy additions for seeding."""
178
+ Casbin format: add_policy(role, resource, action)
179
+ Example: add_policy("admin", "documents", "read")
180
+ """
126
181
  try:
127
182
  result = await self._enforcer.add_policy(*params)
128
183
  # Clear cache when policies are modified
129
184
  if result:
130
185
  await self.clear_cache()
186
+ logger.debug(f"Casbin policy added: {params}")
131
187
  return result
132
- except (ValueError, RuntimeError, AttributeError, TypeError) as e:
133
- # Type 2: Recoverable - return False for Casbin operation errors
134
- logger.warning(f"Failed to add policy: {e}", exc_info=True)
135
- return False
188
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
189
+ # Catching specific exceptions from Casbin operations
190
+ return self._handle_operation_error("add_policy", e, *params)
191
+
192
+ async def add_role_for_user(self, *params: Any) -> bool:
193
+ """
194
+ Assign a role to a user in Casbin.
136
195
 
137
- async def add_role_for_user(self, *params) -> bool:
138
- """Helper to pass-through role additions for seeding."""
196
+ Casbin format: add_role_for_user(user, role)
197
+ This creates a grouping policy: g(user, role)
198
+ """
139
199
  try:
140
200
  result = await self._enforcer.add_role_for_user(*params)
141
201
  # Clear cache when roles are modified
142
202
  if result:
143
203
  await self.clear_cache()
204
+ logger.debug(f"Casbin role assigned: {params}")
144
205
  return result
145
- except (ValueError, RuntimeError, AttributeError) as e:
146
- # Type 2: Recoverable - return False for Casbin operation errors
147
- logger.warning(f"Failed to add role for user: {e}", exc_info=True)
148
- return False
206
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
207
+ # Catching specific exceptions from Casbin operations
208
+ return self._handle_operation_error("add_role_for_user", e, *params)
149
209
 
150
210
  async def save_policy(self) -> bool:
151
- """Helper to pass-through policy saving for seeding."""
211
+ """
212
+ Persist Casbin policies to storage (MongoDB via MotorAdapter).
213
+
214
+ Returns:
215
+ True if saved successfully, False otherwise
216
+ """
152
217
  try:
153
218
  result = await self._enforcer.save_policy()
154
219
  # Clear cache when policies are saved
155
220
  if result:
156
221
  await self.clear_cache()
222
+ logger.debug("Casbin policies saved to storage")
157
223
  return result
158
- except (ValueError, RuntimeError, AttributeError) as e:
159
- # Type 2: Recoverable - return False for Casbin operation errors
160
- logger.warning(f"Failed to save policy: {e}", exc_info=True)
161
- return False
224
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
225
+ # Catching specific exceptions from Casbin operations
226
+ return self._handle_operation_error("save_policy", e)
162
227
 
163
- async def has_policy(self, *params) -> bool:
164
- """Check if a policy exists."""
228
+ async def has_policy(self, *params: Any) -> bool:
229
+ """
230
+ Check if a policy exists in Casbin.
231
+
232
+ Casbin format: has_policy(role, resource, action)
233
+ """
165
234
  try:
166
235
  # Run in thread pool to prevent blocking
167
236
  result = await asyncio.to_thread(self._enforcer.has_policy, *params)
168
237
  return result
169
- except (ValueError, RuntimeError, AttributeError) as e:
170
- # Type 2: Recoverable - return False for Casbin operation errors
171
- logger.warning(f"Failed to check policy: {e}", exc_info=True)
238
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
239
+ # Catching specific exceptions from Casbin operations
240
+ self._handle_operation_error("has_policy", e, *params)
172
241
  return False
173
242
 
174
- async def has_role_for_user(self, *params) -> bool:
175
- """Check if a user has a role."""
243
+ async def has_role_for_user(self, *params: Any) -> bool:
244
+ """
245
+ Check if a user has a specific role in Casbin.
246
+
247
+ Casbin format: has_role_for_user(user, role)
248
+ """
176
249
  try:
177
- # Run in thread pool to prevent blocking
178
- result = await asyncio.to_thread(self._enforcer.has_role_for_user, *params)
250
+ # AsyncEnforcer.has_role_for_user is async, await it directly
251
+ result = await self._enforcer.has_role_for_user(*params)
179
252
  return result
180
- except (ValueError, RuntimeError, AttributeError) as e:
181
- # Type 2: Recoverable - return False for Casbin operation errors
182
- logger.warning(f"Failed to check role for user: {e}", exc_info=True)
253
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
254
+ # Catching specific exceptions from Casbin operations
255
+ self._handle_operation_error("has_role_for_user", e, *params)
183
256
  return False
184
257
 
185
- async def remove_role_for_user(self, *params) -> bool:
186
- """Helper to pass-through role removal."""
258
+ async def remove_role_for_user(self, *params: Any) -> bool:
259
+ """
260
+ Remove a role assignment from a user in Casbin.
261
+
262
+ Casbin format: remove_role_for_user(user, role)
263
+ """
187
264
  try:
188
265
  result = await self._enforcer.remove_role_for_user(*params)
189
266
  # Clear cache when roles are modified
190
267
  if result:
191
268
  await self.clear_cache()
269
+ logger.debug(f"Casbin role removed: {params}")
192
270
  return result
193
- except (ValueError, RuntimeError, AttributeError) as e:
194
- # Type 2: Recoverable - return False for Casbin operation errors
195
- logger.warning(f"Failed to remove role for user: {e}", exc_info=True)
196
- return False
271
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
272
+ # Catching specific exceptions from Casbin operations
273
+ return self._handle_operation_error("remove_role_for_user", e, *params)
197
274
 
198
275
 
199
- class OsoAdapter:
276
+ class OsoAdapter(BaseAuthorizationProvider):
200
277
  """
201
- Implements the AuthorizationProvider interface using OSO/Polar.
202
- Uses caching to improve performance and thread pool execution for blocking operations.
278
+ Adapter for OSO Cloud authorization engine.
279
+
280
+ Implements the BaseAuthorizationProvider interface using OSO Cloud or OSO library.
281
+ Uses the Adapter Pattern to wrap OSO without modifying its source code.
282
+
283
+ Design Principles:
284
+ - Fail-Closed Security: Errors deny access
285
+ - Thread Pool Execution: Prevents blocking the event loop
286
+ - Caching: Improves performance for repeated checks
287
+ - Type Marshalling: Converts strings to OSO's TypedObject format
203
288
  """
204
289
 
205
- # Use a string literal for the type hint to prevent module-level import
206
- # Can accept either OSO Cloud client or OSO library client
207
290
  def __init__(self, oso_client: Any):
208
291
  """
209
- Initializes the adapter with a pre-configured OSO client.
210
- Can be either an OSO Cloud client or OSO library client.
292
+ Initialize the OSO adapter.
293
+
294
+ Args:
295
+ oso_client: Pre-configured OSO Cloud client or OSO library instance
296
+
297
+ Raises:
298
+ AuthorizationError: If OSO is not available
211
299
  """
300
+ # Lazy import to allow code to exist without OSO installed
301
+ try:
302
+ import oso_cloud # noqa: F401
303
+ except ImportError:
304
+ try:
305
+ import oso # noqa: F401
306
+ except ImportError as e:
307
+ raise AuthorizationError(
308
+ "OSO library is not installed. "
309
+ "Install with: pip install oso-cloud or pip install oso"
310
+ ) from e
311
+
312
+ super().__init__(engine_name="OSO Cloud")
212
313
  self._oso = oso_client
213
314
  # Cache for authorization results: {(subject, resource, action): (result, timestamp)}
214
- self._cache: Dict[Tuple[str, str, str], Tuple[bool, float]] = {}
315
+ self._cache: dict[tuple[str, str, str], tuple[bool, float]] = {}
215
316
  self._cache_lock = asyncio.Lock()
216
- logger.info(
217
- "✔️ OsoAdapter initialized with async thread pool execution and caching."
218
- )
317
+ self._mark_initialized()
219
318
 
220
319
  async def check(
221
320
  self,
222
321
  subject: str,
223
322
  resource: str,
224
323
  action: str,
225
- user_object: Optional[Dict[str, Any]] = None,
324
+ user_object: dict[str, Any] | None = None,
226
325
  ) -> bool:
227
326
  """
228
- Performs the authorization check using OSO.
229
- Note: OSO's authorize method signature is: authorize(user, permission, resource)
230
- So we map: subject -> user, action -> permission, resource -> resource
231
- Uses thread pool execution to prevent blocking the event loop and caches results.
327
+ Check authorization using OSO's authorize method.
328
+
329
+ Implements fail-closed security: if evaluation fails, access is denied.
330
+ Uses thread pool execution to prevent blocking the event loop.
331
+
332
+ OSO Format: authorize(actor, action, resource)
333
+ - OSO expects objects with .type and .id attributes
334
+ - We marshal strings to TypedObject instances
232
335
  """
336
+ if not self._initialized:
337
+ logger.error("OsoAdapter not initialized - denying access")
338
+ return False
339
+
233
340
  cache_key = (subject, resource, action)
234
341
  current_time = time.time()
235
342
 
@@ -239,23 +346,22 @@ class OsoAdapter:
239
346
  cached_result, cached_time = self._cache[cache_key]
240
347
  # Check if cache entry is still valid
241
348
  if current_time - cached_time < AUTHZ_CACHE_TTL:
242
- logger.debug(
243
- f"Authorization cache HIT for ({subject}, {resource}, {action})"
244
- )
349
+ logger.debug(f"OSO cache HIT for ({subject}, {resource}, {action})")
245
350
  return cached_result
246
351
  # Cache expired, remove it
247
352
  del self._cache[cache_key]
248
353
 
249
354
  try:
250
- # OSO Cloud's authorize method signature is: authorize(actor, action, resource)
251
- # OSO Cloud expects objects with .type and .id attributes, not dicts
252
- # Create simple objects with type and id attributes
355
+ # OSO Cloud expects objects with .type and .id attributes
356
+ # Create typed objects for OSO Cloud
253
357
  class TypedObject:
254
- def __init__(self, type_name, id_value):
358
+ """Helper class to create OSO-compatible typed objects."""
359
+
360
+ def __init__(self, type_name: str, id_value: str):
255
361
  self.type = type_name
256
362
  self.id = id_value
257
363
 
258
- # Create typed objects for OSO Cloud
364
+ # Marshal strings to OSO TypedObject format
259
365
  if isinstance(subject, str):
260
366
  actor = TypedObject("User", subject)
261
367
  else:
@@ -267,8 +373,12 @@ class OsoAdapter:
267
373
  resource_obj = resource
268
374
 
269
375
  # Run in thread pool to prevent blocking the event loop
376
+ # OSO signature: authorize(actor, action, resource)
270
377
  result = await asyncio.to_thread(
271
- self._oso.authorize, actor, action, resource_obj
378
+ self._oso.authorize,
379
+ actor,
380
+ action,
381
+ resource_obj,
272
382
  )
273
383
 
274
384
  # Cache the result
@@ -283,38 +393,43 @@ class OsoAdapter:
283
393
  )[0]
284
394
  del self._cache[oldest_key]
285
395
 
396
+ logger.debug(f"OSO authorization check: {subject} -> {resource}:{action} = {result}")
286
397
  return result
398
+
287
399
  except (
400
+ RuntimeError,
401
+ ValueError,
288
402
  AttributeError,
289
403
  TypeError,
290
- ValueError,
291
- RuntimeError,
404
+ KeyError,
292
405
  ConnectionError,
293
406
  ) as e:
294
- logger.error(
295
- f"OSO 'authorize' check failed for ({subject}, {resource}, {action}): {e}",
296
- exc_info=True,
297
- )
298
- return False
407
+ # Fail-Closed Security: Any exception denies access
408
+ # Catching specific exceptions from OSO operations
409
+ return self._handle_evaluation_error(subject, resource, action, e, "authorize")
299
410
 
300
- async def clear_cache(self):
411
+ async def clear_cache(self) -> None:
301
412
  """
302
- Clears the authorization cache. Useful when policies are updated.
413
+ Clear the authorization cache.
414
+
415
+ Should be called when policies or roles are modified to ensure
416
+ fresh authorization decisions.
303
417
  """
304
418
  async with self._cache_lock:
305
419
  self._cache.clear()
306
- logger.info("Authorization cache cleared.")
420
+ logger.info(f"{self._engine_name} authorization cache cleared")
307
421
 
308
- async def add_policy(self, *params) -> bool:
422
+ async def add_policy(self, *params: Any) -> bool:
309
423
  """
310
- Adds a grants_permission fact in OSO.
311
- Maps Casbin policy (role, object, action) to OSO fact:
312
- grants_permission(role, action, object)
424
+ Add a policy rule to OSO.
425
+
426
+ OSO format: grants_permission(role, action, object)
427
+ Maps from Casbin format: (role, object, action)
313
428
  """
314
429
  try:
315
430
  if len(params) != 3:
316
431
  logger.warning(
317
- f"add_policy expects 3 params (role, object, action), got {len(params)}"
432
+ f"OSO add_policy expects 3 params (role, object, action), got {len(params)}"
318
433
  )
319
434
  return False
320
435
 
@@ -333,9 +448,7 @@ class OsoAdapter:
333
448
  )
334
449
  elif hasattr(self._oso, "register_constant"):
335
450
  # OSO library - we'd need to use a different approach
336
- logger.warning(
337
- "OSO library mode: add_policy needs to be handled via policy files"
338
- )
451
+ logger.warning("OSO library mode: add_policy needs to be handled via policy files")
339
452
  result = True # Assume success for now
340
453
  else:
341
454
  logger.warning("OSO client doesn't support insert() or tell() method")
@@ -344,22 +457,23 @@ class OsoAdapter:
344
457
  # Clear cache when policies are modified
345
458
  if result:
346
459
  await self.clear_cache()
460
+ logger.debug(f"OSO policy added: grants_permission({role}, {act}, {obj})")
347
461
  return result
348
- except (AttributeError, TypeError, ValueError, RuntimeError) as e:
349
- logger.warning(f"Failed to add policy: {e}", exc_info=True)
350
- return False
462
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
463
+ # Catching specific exceptions from OSO operations
464
+ return self._handle_operation_error("add_policy", e, *params)
351
465
 
352
- async def add_role_for_user(self, *params) -> bool:
466
+ async def add_role_for_user(self, *params: Any) -> bool:
353
467
  """
354
- Adds a has_role fact in OSO.
355
- Supports both global roles (2 params: user, role) and resource-based
356
- roles (3 params: user, role, resource).
357
- Maps to OSO fact: has_role(user, role) or has_role(user, role, resource)
468
+ Assign a role to a user in OSO.
469
+
470
+ OSO format: has_role(user, role) or has_role(user, role, resource)
471
+ Supports both global roles (2 params) and resource-based roles (3 params)
358
472
  """
359
473
  try:
360
474
  if len(params) < 2 or len(params) > 3:
361
475
  logger.warning(
362
- f"add_role_for_user expects 2-3 params "
476
+ f"OSO add_role_for_user expects 2-3 params "
363
477
  f"(user, role, [resource]), got {len(params)}"
364
478
  )
365
479
  return False
@@ -406,9 +520,7 @@ class OsoAdapter:
406
520
  self._oso.tell, "has_role", user, role, resource
407
521
  )
408
522
  else:
409
- result = await asyncio.to_thread(
410
- self._oso.tell, "has_role", user, role
411
- )
523
+ result = await asyncio.to_thread(self._oso.tell, "has_role", user, role)
412
524
  elif hasattr(self._oso, "register_constant"):
413
525
  # OSO library - we'd need to use a different approach
414
526
  logger.warning(
@@ -422,19 +534,18 @@ class OsoAdapter:
422
534
  # Clear cache when roles are modified
423
535
  if result:
424
536
  await self.clear_cache()
537
+ logger.debug(
538
+ f"OSO role assigned: has_role({user}, {role}, " f"{resource or 'documents'})"
539
+ )
425
540
  return result
426
- except (
427
- AttributeError,
428
- TypeError,
429
- ValueError,
430
- RuntimeError,
431
- ConnectionError,
432
- ) as e:
433
- logger.warning(f"Failed to add role for user: {e}", exc_info=True)
434
- return False
541
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
542
+ # Catching specific exceptions from OSO operations
543
+ return self._handle_operation_error("add_role_for_user", e, *params)
435
544
 
436
545
  async def save_policy(self) -> bool:
437
546
  """
547
+ Persist OSO policies/facts to storage.
548
+
438
549
  For OSO Cloud, facts are saved automatically.
439
550
  For OSO library, this would save to a file or database.
440
551
  """
@@ -450,14 +561,17 @@ class OsoAdapter:
450
561
  # Clear cache when policies are saved
451
562
  if result:
452
563
  await self.clear_cache()
564
+ logger.debug("OSO policies/facts saved to storage")
453
565
  return result
454
- except (AttributeError, TypeError, ValueError, RuntimeError) as e:
455
- logger.warning(f"Failed to save policy: {e}", exc_info=True)
456
- return False
566
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
567
+ # Catching specific exceptions from OSO operations
568
+ return self._handle_operation_error("save_policy", e)
457
569
 
458
- async def has_policy(self, *params) -> bool:
570
+ async def has_policy(self, *params: Any) -> bool:
459
571
  """
460
- Check if a grants_permission fact exists in OSO.
572
+ Check if a policy exists in OSO.
573
+
574
+ OSO format: grants_permission(role, action, object)
461
575
  """
462
576
  try:
463
577
  if len(params) != 3:
@@ -483,19 +597,16 @@ class OsoAdapter:
483
597
  # For now, return True as a placeholder
484
598
  logger.debug("OSO Cloud: has_policy check not fully implemented")
485
599
  return True
486
- except (
487
- AttributeError,
488
- TypeError,
489
- ValueError,
490
- RuntimeError,
491
- ConnectionError,
492
- ) as e:
493
- logger.warning(f"Failed to check policy: {e}", exc_info=True)
600
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
601
+ # Catching specific exceptions from OSO operations
602
+ self._handle_operation_error("has_policy", e, *params)
494
603
  return False
495
604
 
496
- async def has_role_for_user(self, *params) -> bool:
605
+ async def has_role_for_user(self, *params: Any) -> bool:
497
606
  """
498
- Check if a has_role fact exists in OSO.
607
+ Check if a user has a specific role in OSO.
608
+
609
+ OSO format: has_role(user, role)
499
610
  """
500
611
  try:
501
612
  if len(params) != 2:
@@ -507,9 +618,7 @@ class OsoAdapter:
507
618
  # OSO library - query facts
508
619
  result = await asyncio.to_thread(
509
620
  lambda: list(
510
- self._oso.query_rule(
511
- "has_role", user, role, accept_expression=True
512
- )
621
+ self._oso.query_rule("has_role", user, role, accept_expression=True)
513
622
  )
514
623
  )
515
624
  return len(result) > 0
@@ -524,33 +633,28 @@ class OsoAdapter:
524
633
  # For now, return True as a placeholder
525
634
  logger.debug("OSO Cloud: has_role_for_user check not fully implemented")
526
635
  return True
527
- except (
528
- AttributeError,
529
- TypeError,
530
- ValueError,
531
- RuntimeError,
532
- ConnectionError,
533
- ) as e:
534
- logger.warning(f"Failed to check role for user: {e}", exc_info=True)
636
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
637
+ # Catching specific exceptions from OSO operations
638
+ self._handle_operation_error("has_role_for_user", e, *params)
535
639
  return False
536
640
 
537
- async def remove_role_for_user(self, *params) -> bool:
641
+ async def remove_role_for_user(self, *params: Any) -> bool:
538
642
  """
539
- Removes a has_role fact in OSO.
643
+ Remove a role assignment from a user in OSO.
644
+
645
+ OSO format: remove has_role(user, role)
540
646
  """
541
647
  try:
542
648
  if len(params) != 2:
543
649
  logger.warning(
544
- f"remove_role_for_user expects 2 params (user, role), got {len(params)}"
650
+ f"OSO remove_role_for_user expects 2 params (user, role), got {len(params)}"
545
651
  )
546
652
  return False
547
653
 
548
654
  user, role = params
549
655
  # OSO Cloud uses delete() method
550
656
  if hasattr(self._oso, "delete"):
551
- result = await asyncio.to_thread(
552
- self._oso.delete, "has_role", user, role
553
- )
657
+ result = await asyncio.to_thread(self._oso.delete, "has_role", user, role)
554
658
  else:
555
659
  logger.warning("OSO client doesn't support delete() method")
556
660
  result = False
@@ -558,13 +662,8 @@ class OsoAdapter:
558
662
  # Clear cache when roles are modified
559
663
  if result:
560
664
  await self.clear_cache()
665
+ logger.debug(f"OSO role removed: has_role({user}, {role})")
561
666
  return result
562
- except (
563
- AttributeError,
564
- TypeError,
565
- ValueError,
566
- RuntimeError,
567
- ConnectionError,
568
- ) as e:
569
- logger.warning(f"Failed to remove role for user: {e}", exc_info=True)
570
- return False
667
+ except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError) as e:
668
+ # Catching specific exceptions from OSO operations
669
+ return self._handle_operation_error("remove_role_for_user", e, *params)