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