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.
- mdb_engine/README.md +144 -0
- mdb_engine/__init__.py +37 -0
- mdb_engine/auth/README.md +631 -0
- mdb_engine/auth/__init__.py +128 -0
- mdb_engine/auth/casbin_factory.py +199 -0
- mdb_engine/auth/casbin_models.py +46 -0
- mdb_engine/auth/config_defaults.py +71 -0
- mdb_engine/auth/config_helpers.py +213 -0
- mdb_engine/auth/cookie_utils.py +158 -0
- mdb_engine/auth/decorators.py +350 -0
- mdb_engine/auth/dependencies.py +747 -0
- mdb_engine/auth/helpers.py +64 -0
- mdb_engine/auth/integration.py +578 -0
- mdb_engine/auth/jwt.py +225 -0
- mdb_engine/auth/middleware.py +241 -0
- mdb_engine/auth/oso_factory.py +323 -0
- mdb_engine/auth/provider.py +570 -0
- mdb_engine/auth/restrictions.py +271 -0
- mdb_engine/auth/session_manager.py +477 -0
- mdb_engine/auth/token_lifecycle.py +213 -0
- mdb_engine/auth/token_store.py +289 -0
- mdb_engine/auth/users.py +1516 -0
- mdb_engine/auth/utils.py +614 -0
- mdb_engine/cli/__init__.py +13 -0
- mdb_engine/cli/commands/__init__.py +7 -0
- mdb_engine/cli/commands/generate.py +105 -0
- mdb_engine/cli/commands/migrate.py +83 -0
- mdb_engine/cli/commands/show.py +70 -0
- mdb_engine/cli/commands/validate.py +63 -0
- mdb_engine/cli/main.py +41 -0
- mdb_engine/cli/utils.py +92 -0
- mdb_engine/config.py +217 -0
- mdb_engine/constants.py +160 -0
- mdb_engine/core/README.md +542 -0
- mdb_engine/core/__init__.py +42 -0
- mdb_engine/core/app_registration.py +392 -0
- mdb_engine/core/connection.py +243 -0
- mdb_engine/core/engine.py +749 -0
- mdb_engine/core/index_management.py +162 -0
- mdb_engine/core/manifest.py +2793 -0
- mdb_engine/core/seeding.py +179 -0
- mdb_engine/core/service_initialization.py +355 -0
- mdb_engine/core/types.py +413 -0
- mdb_engine/database/README.md +522 -0
- mdb_engine/database/__init__.py +31 -0
- mdb_engine/database/abstraction.py +635 -0
- mdb_engine/database/connection.py +387 -0
- mdb_engine/database/scoped_wrapper.py +1721 -0
- mdb_engine/embeddings/README.md +184 -0
- mdb_engine/embeddings/__init__.py +62 -0
- mdb_engine/embeddings/dependencies.py +193 -0
- mdb_engine/embeddings/service.py +759 -0
- mdb_engine/exceptions.py +167 -0
- mdb_engine/indexes/README.md +651 -0
- mdb_engine/indexes/__init__.py +21 -0
- mdb_engine/indexes/helpers.py +145 -0
- mdb_engine/indexes/manager.py +895 -0
- mdb_engine/memory/README.md +451 -0
- mdb_engine/memory/__init__.py +30 -0
- mdb_engine/memory/service.py +1285 -0
- mdb_engine/observability/README.md +515 -0
- mdb_engine/observability/__init__.py +42 -0
- mdb_engine/observability/health.py +296 -0
- mdb_engine/observability/logging.py +161 -0
- mdb_engine/observability/metrics.py +297 -0
- mdb_engine/routing/README.md +462 -0
- mdb_engine/routing/__init__.py +73 -0
- mdb_engine/routing/websockets.py +813 -0
- mdb_engine/utils/__init__.py +7 -0
- mdb_engine-0.1.6.dist-info/METADATA +213 -0
- mdb_engine-0.1.6.dist-info/RECORD +75 -0
- mdb_engine-0.1.6.dist-info/WHEEL +5 -0
- mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
- mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
- 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
|