mdb-engine 0.1.6__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 (75) hide show
  1. mdb_engine/README.md +144 -0
  2. mdb_engine/__init__.py +37 -0
  3. mdb_engine/auth/README.md +631 -0
  4. mdb_engine/auth/__init__.py +128 -0
  5. mdb_engine/auth/casbin_factory.py +199 -0
  6. mdb_engine/auth/casbin_models.py +46 -0
  7. mdb_engine/auth/config_defaults.py +71 -0
  8. mdb_engine/auth/config_helpers.py +213 -0
  9. mdb_engine/auth/cookie_utils.py +158 -0
  10. mdb_engine/auth/decorators.py +350 -0
  11. mdb_engine/auth/dependencies.py +747 -0
  12. mdb_engine/auth/helpers.py +64 -0
  13. mdb_engine/auth/integration.py +578 -0
  14. mdb_engine/auth/jwt.py +225 -0
  15. mdb_engine/auth/middleware.py +241 -0
  16. mdb_engine/auth/oso_factory.py +323 -0
  17. mdb_engine/auth/provider.py +570 -0
  18. mdb_engine/auth/restrictions.py +271 -0
  19. mdb_engine/auth/session_manager.py +477 -0
  20. mdb_engine/auth/token_lifecycle.py +213 -0
  21. mdb_engine/auth/token_store.py +289 -0
  22. mdb_engine/auth/users.py +1516 -0
  23. mdb_engine/auth/utils.py +614 -0
  24. mdb_engine/cli/__init__.py +13 -0
  25. mdb_engine/cli/commands/__init__.py +7 -0
  26. mdb_engine/cli/commands/generate.py +105 -0
  27. mdb_engine/cli/commands/migrate.py +83 -0
  28. mdb_engine/cli/commands/show.py +70 -0
  29. mdb_engine/cli/commands/validate.py +63 -0
  30. mdb_engine/cli/main.py +41 -0
  31. mdb_engine/cli/utils.py +92 -0
  32. mdb_engine/config.py +217 -0
  33. mdb_engine/constants.py +160 -0
  34. mdb_engine/core/README.md +542 -0
  35. mdb_engine/core/__init__.py +42 -0
  36. mdb_engine/core/app_registration.py +392 -0
  37. mdb_engine/core/connection.py +243 -0
  38. mdb_engine/core/engine.py +749 -0
  39. mdb_engine/core/index_management.py +162 -0
  40. mdb_engine/core/manifest.py +2793 -0
  41. mdb_engine/core/seeding.py +179 -0
  42. mdb_engine/core/service_initialization.py +355 -0
  43. mdb_engine/core/types.py +413 -0
  44. mdb_engine/database/README.md +522 -0
  45. mdb_engine/database/__init__.py +31 -0
  46. mdb_engine/database/abstraction.py +635 -0
  47. mdb_engine/database/connection.py +387 -0
  48. mdb_engine/database/scoped_wrapper.py +1721 -0
  49. mdb_engine/embeddings/README.md +184 -0
  50. mdb_engine/embeddings/__init__.py +62 -0
  51. mdb_engine/embeddings/dependencies.py +193 -0
  52. mdb_engine/embeddings/service.py +759 -0
  53. mdb_engine/exceptions.py +167 -0
  54. mdb_engine/indexes/README.md +651 -0
  55. mdb_engine/indexes/__init__.py +21 -0
  56. mdb_engine/indexes/helpers.py +145 -0
  57. mdb_engine/indexes/manager.py +895 -0
  58. mdb_engine/memory/README.md +451 -0
  59. mdb_engine/memory/__init__.py +30 -0
  60. mdb_engine/memory/service.py +1285 -0
  61. mdb_engine/observability/README.md +515 -0
  62. mdb_engine/observability/__init__.py +42 -0
  63. mdb_engine/observability/health.py +296 -0
  64. mdb_engine/observability/logging.py +161 -0
  65. mdb_engine/observability/metrics.py +297 -0
  66. mdb_engine/routing/README.md +462 -0
  67. mdb_engine/routing/__init__.py +73 -0
  68. mdb_engine/routing/websockets.py +813 -0
  69. mdb_engine/utils/__init__.py +7 -0
  70. mdb_engine-0.1.6.dist-info/METADATA +213 -0
  71. mdb_engine-0.1.6.dist-info/RECORD +75 -0
  72. mdb_engine-0.1.6.dist-info/WHEEL +5 -0
  73. mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
  74. mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
  75. mdb_engine-0.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,570 @@
1
+ """
2
+ Authorization Provider Interface
3
+
4
+ Defines the pluggable Authorization (AuthZ) interface for the platform.
5
+
6
+ This module is part of MDB_ENGINE - MongoDB Engine.
7
+ """
8
+
9
+ from __future__ import \
10
+ annotations # MUST be first import for string type hints
11
+
12
+ import asyncio
13
+ import logging
14
+ import time
15
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Protocol, Tuple
16
+
17
+ from ..constants import AUTHZ_CACHE_TTL, MAX_CACHE_SIZE
18
+
19
+ if TYPE_CHECKING:
20
+ import casbin
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class AuthorizationProvider(Protocol):
26
+ """
27
+ Defines the "contract" for any pluggable authorization provider.
28
+ """
29
+
30
+ async def check(
31
+ self,
32
+ subject: str,
33
+ resource: str,
34
+ action: str,
35
+ user_object: Optional[Dict[str, Any]] = None,
36
+ ) -> bool:
37
+ """
38
+ Checks if a subject is allowed to perform an action on a resource.
39
+ """
40
+ ...
41
+
42
+
43
+ class CasbinAdapter:
44
+ """
45
+ Implements the AuthorizationProvider interface using the Casbin AsyncEnforcer.
46
+ Uses thread pool execution and caching to prevent blocking the event loop.
47
+ """
48
+
49
+ # Use a string literal for the type hint to prevent module-level import
50
+ def __init__(self, enforcer: casbin.AsyncEnforcer):
51
+ """
52
+ Initializes the adapter with a pre-configured Casbin AsyncEnforcer.
53
+ """
54
+ self._enforcer = enforcer
55
+ # Cache for authorization results: {(subject, resource, action): (result, timestamp)}
56
+ self._cache: Dict[Tuple[str, str, str], Tuple[bool, float]] = {}
57
+ self._cache_lock = asyncio.Lock()
58
+ logger.info(
59
+ "✔️ CasbinAdapter initialized with async thread pool execution and caching."
60
+ )
61
+
62
+ async def check(
63
+ self,
64
+ subject: str,
65
+ resource: str,
66
+ action: str,
67
+ user_object: Optional[Dict[str, Any]] = None,
68
+ ) -> bool:
69
+ """
70
+ Performs the authorization check using the wrapped enforcer.
71
+ Uses thread pool execution to prevent blocking the event loop and caches results.
72
+ """
73
+ cache_key = (subject, resource, action)
74
+ current_time = time.time()
75
+
76
+ # Check cache first
77
+ async with self._cache_lock:
78
+ if cache_key in self._cache:
79
+ cached_result, cached_time = self._cache[cache_key]
80
+ # Check if cache entry is still valid
81
+ if current_time - cached_time < AUTHZ_CACHE_TTL:
82
+ logger.debug(
83
+ f"Authorization cache HIT for ({subject}, {resource}, {action})"
84
+ )
85
+ return cached_result
86
+ # Cache expired, remove it
87
+ del self._cache[cache_key]
88
+
89
+ try:
90
+ # The .enforce() method on AsyncEnforcer is synchronous and blocks the event loop.
91
+ # Run it in a thread pool to prevent blocking.
92
+ result = await asyncio.to_thread(
93
+ self._enforcer.enforce, subject, resource, action
94
+ )
95
+
96
+ # Cache the result
97
+ async with self._cache_lock:
98
+ self._cache[cache_key] = (result, current_time)
99
+ # Limit cache size to prevent memory issues
100
+ if len(self._cache) > MAX_CACHE_SIZE:
101
+ # Remove oldest entries (simple FIFO eviction)
102
+ oldest_key = min(
103
+ self._cache.items(),
104
+ key=lambda x: x[1][1], # Compare by timestamp
105
+ )[0]
106
+ del self._cache[oldest_key]
107
+
108
+ 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
+
116
+ async def clear_cache(self):
117
+ """
118
+ Clears the authorization cache. Useful when policies are updated.
119
+ """
120
+ async with self._cache_lock:
121
+ self._cache.clear()
122
+ logger.info("Authorization cache cleared.")
123
+
124
+ async def add_policy(self, *params) -> bool:
125
+ """Helper to pass-through policy additions for seeding."""
126
+ try:
127
+ result = await self._enforcer.add_policy(*params)
128
+ # Clear cache when policies are modified
129
+ if result:
130
+ await self.clear_cache()
131
+ 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
136
+
137
+ async def add_role_for_user(self, *params) -> bool:
138
+ """Helper to pass-through role additions for seeding."""
139
+ try:
140
+ result = await self._enforcer.add_role_for_user(*params)
141
+ # Clear cache when roles are modified
142
+ if result:
143
+ await self.clear_cache()
144
+ 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
149
+
150
+ async def save_policy(self) -> bool:
151
+ """Helper to pass-through policy saving for seeding."""
152
+ try:
153
+ result = await self._enforcer.save_policy()
154
+ # Clear cache when policies are saved
155
+ if result:
156
+ await self.clear_cache()
157
+ 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
162
+
163
+ async def has_policy(self, *params) -> bool:
164
+ """Check if a policy exists."""
165
+ try:
166
+ # Run in thread pool to prevent blocking
167
+ result = await asyncio.to_thread(self._enforcer.has_policy, *params)
168
+ 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)
172
+ return False
173
+
174
+ async def has_role_for_user(self, *params) -> bool:
175
+ """Check if a user has a role."""
176
+ try:
177
+ # Run in thread pool to prevent blocking
178
+ result = await asyncio.to_thread(self._enforcer.has_role_for_user, *params)
179
+ 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)
183
+ return False
184
+
185
+ async def remove_role_for_user(self, *params) -> bool:
186
+ """Helper to pass-through role removal."""
187
+ try:
188
+ result = await self._enforcer.remove_role_for_user(*params)
189
+ # Clear cache when roles are modified
190
+ if result:
191
+ await self.clear_cache()
192
+ 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
197
+
198
+
199
+ class OsoAdapter:
200
+ """
201
+ Implements the AuthorizationProvider interface using OSO/Polar.
202
+ Uses caching to improve performance and thread pool execution for blocking operations.
203
+ """
204
+
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
+ def __init__(self, oso_client: Any):
208
+ """
209
+ Initializes the adapter with a pre-configured OSO client.
210
+ Can be either an OSO Cloud client or OSO library client.
211
+ """
212
+ self._oso = oso_client
213
+ # Cache for authorization results: {(subject, resource, action): (result, timestamp)}
214
+ self._cache: Dict[Tuple[str, str, str], Tuple[bool, float]] = {}
215
+ self._cache_lock = asyncio.Lock()
216
+ logger.info(
217
+ "✔️ OsoAdapter initialized with async thread pool execution and caching."
218
+ )
219
+
220
+ async def check(
221
+ self,
222
+ subject: str,
223
+ resource: str,
224
+ action: str,
225
+ user_object: Optional[Dict[str, Any]] = None,
226
+ ) -> bool:
227
+ """
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.
232
+ """
233
+ cache_key = (subject, resource, action)
234
+ current_time = time.time()
235
+
236
+ # Check cache first
237
+ async with self._cache_lock:
238
+ if cache_key in self._cache:
239
+ cached_result, cached_time = self._cache[cache_key]
240
+ # Check if cache entry is still valid
241
+ if current_time - cached_time < AUTHZ_CACHE_TTL:
242
+ logger.debug(
243
+ f"Authorization cache HIT for ({subject}, {resource}, {action})"
244
+ )
245
+ return cached_result
246
+ # Cache expired, remove it
247
+ del self._cache[cache_key]
248
+
249
+ 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
253
+ class TypedObject:
254
+ def __init__(self, type_name, id_value):
255
+ self.type = type_name
256
+ self.id = id_value
257
+
258
+ # Create typed objects for OSO Cloud
259
+ if isinstance(subject, str):
260
+ actor = TypedObject("User", subject)
261
+ else:
262
+ actor = subject
263
+
264
+ if isinstance(resource, str):
265
+ resource_obj = TypedObject("Document", resource)
266
+ else:
267
+ resource_obj = resource
268
+
269
+ # Run in thread pool to prevent blocking the event loop
270
+ result = await asyncio.to_thread(
271
+ self._oso.authorize, actor, action, resource_obj
272
+ )
273
+
274
+ # Cache the result
275
+ async with self._cache_lock:
276
+ self._cache[cache_key] = (result, current_time)
277
+ # Limit cache size to prevent memory issues
278
+ if len(self._cache) > MAX_CACHE_SIZE:
279
+ # Remove oldest entries (simple FIFO eviction)
280
+ oldest_key = min(
281
+ self._cache.items(),
282
+ key=lambda x: x[1][1], # Compare by timestamp
283
+ )[0]
284
+ del self._cache[oldest_key]
285
+
286
+ return result
287
+ except (
288
+ AttributeError,
289
+ TypeError,
290
+ ValueError,
291
+ RuntimeError,
292
+ ConnectionError,
293
+ ) as e:
294
+ logger.error(
295
+ f"OSO 'authorize' check failed for ({subject}, {resource}, {action}): {e}",
296
+ exc_info=True,
297
+ )
298
+ return False
299
+
300
+ async def clear_cache(self):
301
+ """
302
+ Clears the authorization cache. Useful when policies are updated.
303
+ """
304
+ async with self._cache_lock:
305
+ self._cache.clear()
306
+ logger.info("Authorization cache cleared.")
307
+
308
+ async def add_policy(self, *params) -> bool:
309
+ """
310
+ Adds a grants_permission fact in OSO.
311
+ Maps Casbin policy (role, object, action) to OSO fact:
312
+ grants_permission(role, action, object)
313
+ """
314
+ try:
315
+ if len(params) != 3:
316
+ logger.warning(
317
+ f"add_policy expects 3 params (role, object, action), got {len(params)}"
318
+ )
319
+ return False
320
+
321
+ role, obj, act = params
322
+ # OSO fact: grants_permission(role, action, object)
323
+ # OSO Cloud SDK uses insert() method with a list
324
+ if hasattr(self._oso, "insert"):
325
+ # OSO Cloud client - insert fact as a list
326
+ result = await asyncio.to_thread(
327
+ self._oso.insert, ["grants_permission", role, act, obj]
328
+ )
329
+ elif hasattr(self._oso, "tell"):
330
+ # Legacy OSO Cloud SDK
331
+ result = await asyncio.to_thread(
332
+ self._oso.tell, "grants_permission", role, act, obj
333
+ )
334
+ elif hasattr(self._oso, "register_constant"):
335
+ # 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
+ )
339
+ result = True # Assume success for now
340
+ else:
341
+ logger.warning("OSO client doesn't support insert() or tell() method")
342
+ result = False
343
+
344
+ # Clear cache when policies are modified
345
+ if result:
346
+ await self.clear_cache()
347
+ 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
351
+
352
+ async def add_role_for_user(self, *params) -> bool:
353
+ """
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)
358
+ """
359
+ try:
360
+ if len(params) < 2 or len(params) > 3:
361
+ logger.warning(
362
+ f"add_role_for_user expects 2-3 params "
363
+ f"(user, role, [resource]), got {len(params)}"
364
+ )
365
+ return False
366
+
367
+ user, role = params[0], params[1]
368
+ resource = params[2] if len(params) == 3 else None
369
+
370
+ # OSO Cloud SDK uses insert() method with Value objects for typed entities
371
+ if hasattr(self._oso, "insert"):
372
+ try:
373
+ from oso_cloud import Value
374
+
375
+ # OSO Cloud client - insert fact with Value objects
376
+ # User must be a Value object with type "User" and id as the email string
377
+ user_value = Value("User", str(user))
378
+
379
+ if resource is not None:
380
+ # Resource-based role: has_role(user, role, resource)
381
+ # Resource can be a string (resource type) or a Value object
382
+ if isinstance(resource, str):
383
+ resource_value = Value("Document", str(resource))
384
+ else:
385
+ resource_value = resource
386
+ fact = ["has_role", user_value, str(role), resource_value]
387
+ else:
388
+ # Global role: has_role(user, role)
389
+ # For resource-based policies, we still need a resource
390
+ # Default to resource type "documents" if not specified
391
+ resource_value = Value("Document", "documents")
392
+ fact = ["has_role", user_value, str(role), resource_value]
393
+
394
+ result = await asyncio.to_thread(self._oso.insert, fact)
395
+ except ImportError:
396
+ # Fallback if Value not available - try with string
397
+ if resource is not None:
398
+ fact = ["has_role", str(user), str(role), str(resource)]
399
+ else:
400
+ fact = ["has_role", str(user), str(role), "documents"]
401
+ result = await asyncio.to_thread(self._oso.insert, fact)
402
+ elif hasattr(self._oso, "tell"):
403
+ # Legacy OSO Cloud SDK
404
+ if resource is not None:
405
+ result = await asyncio.to_thread(
406
+ self._oso.tell, "has_role", user, role, resource
407
+ )
408
+ else:
409
+ result = await asyncio.to_thread(
410
+ self._oso.tell, "has_role", user, role
411
+ )
412
+ elif hasattr(self._oso, "register_constant"):
413
+ # OSO library - we'd need to use a different approach
414
+ logger.warning(
415
+ "OSO library mode: add_role_for_user needs to be handled via policy files"
416
+ )
417
+ result = True # Assume success for now
418
+ else:
419
+ logger.warning("OSO client doesn't support insert() or tell() method")
420
+ result = False
421
+
422
+ # Clear cache when roles are modified
423
+ if result:
424
+ await self.clear_cache()
425
+ 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
435
+
436
+ async def save_policy(self) -> bool:
437
+ """
438
+ For OSO Cloud, facts are saved automatically.
439
+ For OSO library, this would save to a file or database.
440
+ """
441
+ try:
442
+ # OSO Cloud automatically persists facts, so this is a no-op
443
+ # For OSO library, we might need to implement file/database saving
444
+ if hasattr(self._oso, "save"):
445
+ result = await asyncio.to_thread(self._oso.save)
446
+ else:
447
+ # OSO Cloud - facts are automatically persisted
448
+ result = True
449
+
450
+ # Clear cache when policies are saved
451
+ if result:
452
+ await self.clear_cache()
453
+ 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
457
+
458
+ async def has_policy(self, *params) -> bool:
459
+ """
460
+ Check if a grants_permission fact exists in OSO.
461
+ """
462
+ try:
463
+ if len(params) != 3:
464
+ return False
465
+
466
+ role, obj, act = params
467
+ # For OSO, we'd need to query facts or check if authorize would work
468
+ # This is a simplified check - in practice, you might want to query facts directly
469
+ # For now, we'll attempt an authorize check as a proxy
470
+ # This isn't perfect but provides compatibility
471
+ if hasattr(self._oso, "query"):
472
+ # OSO library - query facts
473
+ result = await asyncio.to_thread(
474
+ lambda: list(
475
+ self._oso.query_rule(
476
+ "grants_permission", role, act, obj, accept_expression=True
477
+ )
478
+ )
479
+ )
480
+ return len(result) > 0
481
+ else:
482
+ # OSO Cloud - we'd need to use the API to check facts
483
+ # For now, return True as a placeholder
484
+ logger.debug("OSO Cloud: has_policy check not fully implemented")
485
+ 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)
494
+ return False
495
+
496
+ async def has_role_for_user(self, *params) -> bool:
497
+ """
498
+ Check if a has_role fact exists in OSO.
499
+ """
500
+ try:
501
+ if len(params) != 2:
502
+ return False
503
+
504
+ user, role = params
505
+ # For OSO, we'd need to query facts
506
+ if hasattr(self._oso, "query_rule"):
507
+ # OSO library - query facts
508
+ result = await asyncio.to_thread(
509
+ lambda: list(
510
+ self._oso.query_rule(
511
+ "has_role", user, role, accept_expression=True
512
+ )
513
+ )
514
+ )
515
+ return len(result) > 0
516
+ elif hasattr(self._oso, "query"):
517
+ # Alternative query method
518
+ result = await asyncio.to_thread(
519
+ lambda: list(self._oso.query("has_role", user, role))
520
+ )
521
+ return len(result) > 0
522
+ else:
523
+ # OSO Cloud - we'd need to use the API to check facts
524
+ # For now, return True as a placeholder
525
+ logger.debug("OSO Cloud: has_role_for_user check not fully implemented")
526
+ 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)
535
+ return False
536
+
537
+ async def remove_role_for_user(self, *params) -> bool:
538
+ """
539
+ Removes a has_role fact in OSO.
540
+ """
541
+ try:
542
+ if len(params) != 2:
543
+ logger.warning(
544
+ f"remove_role_for_user expects 2 params (user, role), got {len(params)}"
545
+ )
546
+ return False
547
+
548
+ user, role = params
549
+ # OSO Cloud uses delete() method
550
+ if hasattr(self._oso, "delete"):
551
+ result = await asyncio.to_thread(
552
+ self._oso.delete, "has_role", user, role
553
+ )
554
+ else:
555
+ logger.warning("OSO client doesn't support delete() method")
556
+ result = False
557
+
558
+ # Clear cache when roles are modified
559
+ if result:
560
+ await self.clear_cache()
561
+ 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