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.
@@ -15,15 +15,22 @@ from typing import TYPE_CHECKING, Any, Optional, Protocol
15
15
 
16
16
  from ..constants import AUTHZ_CACHE_TTL, MAX_CACHE_SIZE
17
17
 
18
+ # Import base class
19
+ from .base import AuthorizationError, BaseAuthorizationProvider
20
+
18
21
  if TYPE_CHECKING:
19
22
  import casbin
20
23
 
21
24
  logger = logging.getLogger(__name__)
22
25
 
23
26
 
27
+ # Keep Protocol for backward compatibility and type checking
24
28
  class AuthorizationProvider(Protocol):
25
29
  """
26
- 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.
27
34
  """
28
35
 
29
36
  async def check(
@@ -39,22 +46,45 @@ class AuthorizationProvider(Protocol):
39
46
  ...
40
47
 
41
48
 
42
- class CasbinAdapter:
49
+ class CasbinAdapter(BaseAuthorizationProvider):
43
50
  """
44
- Implements the AuthorizationProvider interface using the Casbin AsyncEnforcer.
45
- 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
46
61
  """
47
62
 
48
- # Use a string literal for the type hint to prevent module-level import
49
63
  def __init__(self, enforcer: casbin.AsyncEnforcer):
50
64
  """
51
- 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
52
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")
53
83
  self._enforcer = enforcer
54
84
  # Cache for authorization results: {(subject, resource, action): (result, timestamp)}
55
85
  self._cache: dict[tuple[str, str, str], tuple[bool, float]] = {}
56
86
  self._cache_lock = asyncio.Lock()
57
- logger.info("✔️ CasbinAdapter initialized with async thread pool execution and caching.")
87
+ self._mark_initialized()
58
88
 
59
89
  async def check(
60
90
  self,
@@ -64,9 +94,20 @@ class CasbinAdapter:
64
94
  user_object: Optional[dict[str, Any]] = None,
65
95
  ) -> bool:
66
96
  """
67
- Performs the authorization check using the wrapped enforcer.
68
- 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
69
106
  """
107
+ if not self._initialized:
108
+ logger.error("CasbinAdapter not initialized - denying access")
109
+ return False
110
+
70
111
  cache_key = (subject, resource, action)
71
112
  current_time = time.time()
72
113
 
@@ -76,15 +117,21 @@ class CasbinAdapter:
76
117
  cached_result, cached_time = self._cache[cache_key]
77
118
  # Check if cache entry is still valid
78
119
  if current_time - cached_time < AUTHZ_CACHE_TTL:
79
- logger.debug(f"Authorization cache HIT for ({subject}, {resource}, {action})")
120
+ logger.debug(f"Casbin cache HIT for ({subject}, {resource}, {action})")
80
121
  return cached_result
81
122
  # Cache expired, remove it
82
123
  del self._cache[cache_key]
83
124
 
84
125
  try:
85
- # The .enforce() method on AsyncEnforcer is synchronous and blocks the event loop.
126
+ # Casbin's enforce() is synchronous and blocks the event loop.
86
127
  # Run it in a thread pool to prevent blocking.
87
- result = await asyncio.to_thread(self._enforcer.enforce, subject, resource, action)
128
+ # Casbin order: (subject, object, action)
129
+ result = await asyncio.to_thread(
130
+ self._enforcer.enforce,
131
+ subject, # Casbin subject
132
+ resource, # Casbin object
133
+ action, # Casbin action
134
+ )
88
135
 
89
136
  # Cache the result
90
137
  async with self._cache_lock:
@@ -98,115 +145,176 @@ class CasbinAdapter:
98
145
  )[0]
99
146
  del self._cache[oldest_key]
100
147
 
148
+ logger.debug(f"Casbin authorization check: {subject} -> {resource}:{action} = {result}")
101
149
  return result
102
- except (AttributeError, TypeError, ValueError, RuntimeError) as e:
103
- logger.error(
104
- f"Casbin 'enforce' check failed for ({subject}, {resource}, {action}): {e}",
105
- exc_info=True,
106
- )
107
- return False
108
150
 
109
- 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:
110
164
  """
111
- 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.
112
169
  """
113
170
  async with self._cache_lock:
114
171
  self._cache.clear()
115
- logger.info("Authorization cache cleared.")
172
+ logger.info(f"{self._engine_name} authorization cache cleared")
116
173
 
117
- async def add_policy(self, *params) -> bool:
118
- """Helper to pass-through policy additions for seeding."""
174
+ async def add_policy(self, *params: Any) -> bool:
175
+ """
176
+ Add a policy rule to Casbin.
177
+
178
+ Casbin format: add_policy(role, resource, action)
179
+ Example: add_policy("admin", "documents", "read")
180
+ """
119
181
  try:
120
182
  result = await self._enforcer.add_policy(*params)
121
183
  # Clear cache when policies are modified
122
184
  if result:
123
185
  await self.clear_cache()
186
+ logger.debug(f"Casbin policy added: {params}")
124
187
  return result
125
- except (ValueError, RuntimeError, AttributeError, TypeError) as e:
126
- # Type 2: Recoverable - return False for Casbin operation errors
127
- logger.warning(f"Failed to add policy: {e}", exc_info=True)
128
- 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.
129
195
 
130
- async def add_role_for_user(self, *params) -> bool:
131
- """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
+ """
132
199
  try:
133
200
  result = await self._enforcer.add_role_for_user(*params)
134
201
  # Clear cache when roles are modified
135
202
  if result:
136
203
  await self.clear_cache()
204
+ logger.debug(f"Casbin role assigned: {params}")
137
205
  return result
138
- except (ValueError, RuntimeError, AttributeError) as e:
139
- # Type 2: Recoverable - return False for Casbin operation errors
140
- logger.warning(f"Failed to add role for user: {e}", exc_info=True)
141
- 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)
142
209
 
143
210
  async def save_policy(self) -> bool:
144
- """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
+ """
145
217
  try:
146
218
  result = await self._enforcer.save_policy()
147
219
  # Clear cache when policies are saved
148
220
  if result:
149
221
  await self.clear_cache()
222
+ logger.debug("Casbin policies saved to storage")
150
223
  return result
151
- except (ValueError, RuntimeError, AttributeError) as e:
152
- # Type 2: Recoverable - return False for Casbin operation errors
153
- logger.warning(f"Failed to save policy: {e}", exc_info=True)
154
- 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)
227
+
228
+ async def has_policy(self, *params: Any) -> bool:
229
+ """
230
+ Check if a policy exists in Casbin.
155
231
 
156
- async def has_policy(self, *params) -> bool:
157
- """Check if a policy exists."""
232
+ Casbin format: has_policy(role, resource, action)
233
+ """
158
234
  try:
159
235
  # Run in thread pool to prevent blocking
160
236
  result = await asyncio.to_thread(self._enforcer.has_policy, *params)
161
237
  return result
162
- except (ValueError, RuntimeError, AttributeError) as e:
163
- # Type 2: Recoverable - return False for Casbin operation errors
164
- 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)
165
241
  return False
166
242
 
167
- async def has_role_for_user(self, *params) -> bool:
168
- """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
+ """
169
249
  try:
170
- # Run in thread pool to prevent blocking
171
- 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)
172
252
  return result
173
- except (ValueError, RuntimeError, AttributeError) as e:
174
- # Type 2: Recoverable - return False for Casbin operation errors
175
- 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)
176
256
  return False
177
257
 
178
- async def remove_role_for_user(self, *params) -> bool:
179
- """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
+ """
180
264
  try:
181
265
  result = await self._enforcer.remove_role_for_user(*params)
182
266
  # Clear cache when roles are modified
183
267
  if result:
184
268
  await self.clear_cache()
269
+ logger.debug(f"Casbin role removed: {params}")
185
270
  return result
186
- except (ValueError, RuntimeError, AttributeError) as e:
187
- # Type 2: Recoverable - return False for Casbin operation errors
188
- logger.warning(f"Failed to remove role for user: {e}", exc_info=True)
189
- 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)
190
274
 
191
275
 
192
- class OsoAdapter:
276
+ class OsoAdapter(BaseAuthorizationProvider):
193
277
  """
194
- Implements the AuthorizationProvider interface using OSO/Polar.
195
- 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
196
288
  """
197
289
 
198
- # Use a string literal for the type hint to prevent module-level import
199
- # Can accept either OSO Cloud client or OSO library client
200
290
  def __init__(self, oso_client: Any):
201
291
  """
202
- Initializes the adapter with a pre-configured OSO client.
203
- 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
204
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")
205
313
  self._oso = oso_client
206
314
  # Cache for authorization results: {(subject, resource, action): (result, timestamp)}
207
315
  self._cache: dict[tuple[str, str, str], tuple[bool, float]] = {}
208
316
  self._cache_lock = asyncio.Lock()
209
- logger.info("✔️ OsoAdapter initialized with async thread pool execution and caching.")
317
+ self._mark_initialized()
210
318
 
211
319
  async def check(
212
320
  self,
@@ -216,11 +324,19 @@ class OsoAdapter:
216
324
  user_object: Optional[dict[str, Any]] = None,
217
325
  ) -> bool:
218
326
  """
219
- Performs the authorization check using OSO.
220
- Note: OSO's authorize method signature is: authorize(user, permission, resource)
221
- So we map: subject -> user, action -> permission, resource -> resource
222
- 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
223
335
  """
336
+ if not self._initialized:
337
+ logger.error("OsoAdapter not initialized - denying access")
338
+ return False
339
+
224
340
  cache_key = (subject, resource, action)
225
341
  current_time = time.time()
226
342
 
@@ -230,21 +346,22 @@ class OsoAdapter:
230
346
  cached_result, cached_time = self._cache[cache_key]
231
347
  # Check if cache entry is still valid
232
348
  if current_time - cached_time < AUTHZ_CACHE_TTL:
233
- logger.debug(f"Authorization cache HIT for ({subject}, {resource}, {action})")
349
+ logger.debug(f"OSO cache HIT for ({subject}, {resource}, {action})")
234
350
  return cached_result
235
351
  # Cache expired, remove it
236
352
  del self._cache[cache_key]
237
353
 
238
354
  try:
239
- # OSO Cloud's authorize method signature is: authorize(actor, action, resource)
240
- # OSO Cloud expects objects with .type and .id attributes, not dicts
241
- # 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
242
357
  class TypedObject:
243
- 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):
244
361
  self.type = type_name
245
362
  self.id = id_value
246
363
 
247
- # Create typed objects for OSO Cloud
364
+ # Marshal strings to OSO TypedObject format
248
365
  if isinstance(subject, str):
249
366
  actor = TypedObject("User", subject)
250
367
  else:
@@ -256,7 +373,13 @@ class OsoAdapter:
256
373
  resource_obj = resource
257
374
 
258
375
  # Run in thread pool to prevent blocking the event loop
259
- result = await asyncio.to_thread(self._oso.authorize, actor, action, resource_obj)
376
+ # OSO signature: authorize(actor, action, resource)
377
+ result = await asyncio.to_thread(
378
+ self._oso.authorize,
379
+ actor,
380
+ action,
381
+ resource_obj,
382
+ )
260
383
 
261
384
  # Cache the result
262
385
  async with self._cache_lock:
@@ -270,38 +393,43 @@ class OsoAdapter:
270
393
  )[0]
271
394
  del self._cache[oldest_key]
272
395
 
396
+ logger.debug(f"OSO authorization check: {subject} -> {resource}:{action} = {result}")
273
397
  return result
398
+
274
399
  except (
400
+ RuntimeError,
401
+ ValueError,
275
402
  AttributeError,
276
403
  TypeError,
277
- ValueError,
278
- RuntimeError,
404
+ KeyError,
279
405
  ConnectionError,
280
406
  ) as e:
281
- logger.error(
282
- f"OSO 'authorize' check failed for ({subject}, {resource}, {action}): {e}",
283
- exc_info=True,
284
- )
285
- 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")
286
410
 
287
- async def clear_cache(self):
411
+ async def clear_cache(self) -> None:
288
412
  """
289
- 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.
290
417
  """
291
418
  async with self._cache_lock:
292
419
  self._cache.clear()
293
- logger.info("Authorization cache cleared.")
420
+ logger.info(f"{self._engine_name} authorization cache cleared")
294
421
 
295
- async def add_policy(self, *params) -> bool:
422
+ async def add_policy(self, *params: Any) -> bool:
296
423
  """
297
- Adds a grants_permission fact in OSO.
298
- Maps Casbin policy (role, object, action) to OSO fact:
299
- 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)
300
428
  """
301
429
  try:
302
430
  if len(params) != 3:
303
431
  logger.warning(
304
- 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)}"
305
433
  )
306
434
  return False
307
435
 
@@ -329,22 +457,23 @@ class OsoAdapter:
329
457
  # Clear cache when policies are modified
330
458
  if result:
331
459
  await self.clear_cache()
460
+ logger.debug(f"OSO policy added: grants_permission({role}, {act}, {obj})")
332
461
  return result
333
- except (AttributeError, TypeError, ValueError, RuntimeError) as e:
334
- logger.warning(f"Failed to add policy: {e}", exc_info=True)
335
- 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)
336
465
 
337
- async def add_role_for_user(self, *params) -> bool:
466
+ async def add_role_for_user(self, *params: Any) -> bool:
338
467
  """
339
- Adds a has_role fact in OSO.
340
- Supports both global roles (2 params: user, role) and resource-based
341
- roles (3 params: user, role, resource).
342
- 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)
343
472
  """
344
473
  try:
345
474
  if len(params) < 2 or len(params) > 3:
346
475
  logger.warning(
347
- f"add_role_for_user expects 2-3 params "
476
+ f"OSO add_role_for_user expects 2-3 params "
348
477
  f"(user, role, [resource]), got {len(params)}"
349
478
  )
350
479
  return False
@@ -405,19 +534,18 @@ class OsoAdapter:
405
534
  # Clear cache when roles are modified
406
535
  if result:
407
536
  await self.clear_cache()
537
+ logger.debug(
538
+ f"OSO role assigned: has_role({user}, {role}, " f"{resource or 'documents'})"
539
+ )
408
540
  return result
409
- except (
410
- AttributeError,
411
- TypeError,
412
- ValueError,
413
- RuntimeError,
414
- ConnectionError,
415
- ) as e:
416
- logger.warning(f"Failed to add role for user: {e}", exc_info=True)
417
- 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)
418
544
 
419
545
  async def save_policy(self) -> bool:
420
546
  """
547
+ Persist OSO policies/facts to storage.
548
+
421
549
  For OSO Cloud, facts are saved automatically.
422
550
  For OSO library, this would save to a file or database.
423
551
  """
@@ -433,14 +561,17 @@ class OsoAdapter:
433
561
  # Clear cache when policies are saved
434
562
  if result:
435
563
  await self.clear_cache()
564
+ logger.debug("OSO policies/facts saved to storage")
436
565
  return result
437
- except (AttributeError, TypeError, ValueError, RuntimeError) as e:
438
- logger.warning(f"Failed to save policy: {e}", exc_info=True)
439
- 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)
440
569
 
441
- async def has_policy(self, *params) -> bool:
570
+ async def has_policy(self, *params: Any) -> bool:
442
571
  """
443
- 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)
444
575
  """
445
576
  try:
446
577
  if len(params) != 3:
@@ -466,19 +597,16 @@ class OsoAdapter:
466
597
  # For now, return True as a placeholder
467
598
  logger.debug("OSO Cloud: has_policy check not fully implemented")
468
599
  return True
469
- except (
470
- AttributeError,
471
- TypeError,
472
- ValueError,
473
- RuntimeError,
474
- ConnectionError,
475
- ) as e:
476
- 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)
477
603
  return False
478
604
 
479
- async def has_role_for_user(self, *params) -> bool:
605
+ async def has_role_for_user(self, *params: Any) -> bool:
480
606
  """
481
- 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)
482
610
  """
483
611
  try:
484
612
  if len(params) != 2:
@@ -505,24 +633,21 @@ class OsoAdapter:
505
633
  # For now, return True as a placeholder
506
634
  logger.debug("OSO Cloud: has_role_for_user check not fully implemented")
507
635
  return True
508
- except (
509
- AttributeError,
510
- TypeError,
511
- ValueError,
512
- RuntimeError,
513
- ConnectionError,
514
- ) as e:
515
- 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)
516
639
  return False
517
640
 
518
- async def remove_role_for_user(self, *params) -> bool:
641
+ async def remove_role_for_user(self, *params: Any) -> bool:
519
642
  """
520
- 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)
521
646
  """
522
647
  try:
523
648
  if len(params) != 2:
524
649
  logger.warning(
525
- 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)}"
526
651
  )
527
652
  return False
528
653
 
@@ -537,13 +662,8 @@ class OsoAdapter:
537
662
  # Clear cache when roles are modified
538
663
  if result:
539
664
  await self.clear_cache()
665
+ logger.debug(f"OSO role removed: has_role({user}, {role})")
540
666
  return result
541
- except (
542
- AttributeError,
543
- TypeError,
544
- ValueError,
545
- RuntimeError,
546
- ConnectionError,
547
- ) as e:
548
- logger.warning(f"Failed to remove role for user: {e}", exc_info=True)
549
- 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)