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.
- mdb_engine/__init__.py +71 -10
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +125 -11
- mdb_engine/auth/__init__.py +7 -1
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +258 -59
- mdb_engine/auth/dependencies.py +10 -5
- mdb_engine/auth/integration.py +23 -7
- mdb_engine/auth/oso_factory.py +2 -2
- mdb_engine/auth/provider.py +263 -143
- mdb_engine/core/engine.py +307 -6
- mdb_engine/core/manifest.py +35 -15
- mdb_engine/database/README.md +28 -1
- mdb_engine/dependencies.py +426 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +248 -0
- mdb_engine/di/providers.py +205 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +22 -23
- mdb_engine/embeddings/dependencies.py +37 -152
- mdb_engine/repositories/__init__.py +34 -0
- mdb_engine/repositories/base.py +325 -0
- mdb_engine/repositories/mongo.py +233 -0
- mdb_engine/repositories/unit_of_work.py +166 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/METADATA +42 -14
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/RECORD +31 -20
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
mdb_engine/auth/provider.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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"
|
|
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
|
-
#
|
|
126
|
+
# Casbin's enforce() is synchronous and blocks the event loop.
|
|
86
127
|
# Run it in a thread pool to prevent blocking.
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
172
|
+
logger.info(f"{self._engine_name} authorization cache cleared")
|
|
116
173
|
|
|
117
|
-
async def add_policy(self, *params) -> bool:
|
|
118
|
-
"""
|
|
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 (
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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,
|
|
139
|
-
#
|
|
140
|
-
|
|
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
|
-
"""
|
|
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,
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
157
|
-
"""
|
|
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,
|
|
163
|
-
#
|
|
164
|
-
|
|
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
|
-
"""
|
|
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
|
-
#
|
|
171
|
-
result = await
|
|
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,
|
|
174
|
-
#
|
|
175
|
-
|
|
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
|
-
"""
|
|
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,
|
|
187
|
-
#
|
|
188
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
Uses thread pool execution to prevent blocking the event loop
|
|
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"
|
|
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
|
|
240
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
RuntimeError,
|
|
404
|
+
KeyError,
|
|
279
405
|
ConnectionError,
|
|
280
406
|
) as e:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
298
|
-
|
|
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,
|
|
334
|
-
|
|
335
|
-
return
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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,
|
|
438
|
-
|
|
439
|
-
return
|
|
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
|
|
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
|
-
|
|
471
|
-
|
|
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
|
|
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
|
-
|
|
510
|
-
|
|
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
|
-
|
|
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
|
-
|
|
543
|
-
|
|
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)
|