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,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OSO Provider Factory
|
|
3
|
+
|
|
4
|
+
Provides helper functions to auto-initialize OSO Cloud authorization provider
|
|
5
|
+
from manifest configuration.
|
|
6
|
+
|
|
7
|
+
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .provider import OsoAdapter
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def create_oso_cloud_client(
|
|
23
|
+
api_key: Optional[str] = None,
|
|
24
|
+
url: Optional[str] = None,
|
|
25
|
+
max_retries: int = 3,
|
|
26
|
+
retry_delay: float = 2.0,
|
|
27
|
+
) -> Any:
|
|
28
|
+
"""
|
|
29
|
+
Create an OSO Cloud client instance with retry logic for Dev Server.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
api_key: OSO Cloud API key. If None, reads from OSO_AUTH env var.
|
|
33
|
+
url: OSO Cloud URL. If None, reads from OSO_URL env var.
|
|
34
|
+
Defaults to production cloud.osohq.com if not set.
|
|
35
|
+
max_retries: Maximum number of retry attempts (default: 3)
|
|
36
|
+
retry_delay: Delay between retries in seconds (default: 2.0)
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
OSO Cloud client instance
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ImportError: If oso-cloud is not installed
|
|
43
|
+
ValueError: If API key is not provided
|
|
44
|
+
"""
|
|
45
|
+
import asyncio
|
|
46
|
+
|
|
47
|
+
# Import OSO Cloud SDK - the class is named "Oso"
|
|
48
|
+
try:
|
|
49
|
+
from oso_cloud import Oso
|
|
50
|
+
|
|
51
|
+
logger.debug("✅ Imported Oso from oso_cloud")
|
|
52
|
+
except ImportError:
|
|
53
|
+
raise ImportError(
|
|
54
|
+
"OSO Cloud SDK not installed. Install with: pip install oso-cloud"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Get API key from parameter or environment
|
|
58
|
+
if not api_key:
|
|
59
|
+
api_key = os.getenv("OSO_AUTH")
|
|
60
|
+
|
|
61
|
+
if not api_key:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"OSO Cloud API key not provided. "
|
|
64
|
+
"Set OSO_AUTH environment variable or provide api_key parameter."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Get URL from parameter or environment
|
|
68
|
+
if not url:
|
|
69
|
+
url = os.getenv("OSO_URL")
|
|
70
|
+
|
|
71
|
+
# Initialize OSO Cloud client with explicit parameters
|
|
72
|
+
# The Oso() constructor requires api_key parameter
|
|
73
|
+
# For Dev Server, we may need to retry if it's not ready yet
|
|
74
|
+
last_error = None
|
|
75
|
+
for attempt in range(max_retries):
|
|
76
|
+
try:
|
|
77
|
+
if url:
|
|
78
|
+
oso_client = Oso(api_key=api_key, url=url)
|
|
79
|
+
else:
|
|
80
|
+
oso_client = Oso(api_key=api_key)
|
|
81
|
+
|
|
82
|
+
# Note: OSO client creation doesn't actually connect to the server
|
|
83
|
+
# The connection happens on first API call, so we'll catch errors then
|
|
84
|
+
logger.info(
|
|
85
|
+
f"✅ OSO Cloud client created successfully (URL: {url or 'default'})"
|
|
86
|
+
)
|
|
87
|
+
if url:
|
|
88
|
+
logger.info(f" Using OSO Dev Server at: {url}")
|
|
89
|
+
return oso_client
|
|
90
|
+
|
|
91
|
+
except (
|
|
92
|
+
ValueError,
|
|
93
|
+
TypeError,
|
|
94
|
+
AttributeError,
|
|
95
|
+
RuntimeError,
|
|
96
|
+
ConnectionError,
|
|
97
|
+
) as e:
|
|
98
|
+
last_error = e
|
|
99
|
+
if attempt < max_retries - 1:
|
|
100
|
+
logger.warning(
|
|
101
|
+
f"Failed to create OSO Cloud client "
|
|
102
|
+
f"(attempt {attempt + 1}/{max_retries}): {e}. "
|
|
103
|
+
f"Retrying in {retry_delay} seconds..."
|
|
104
|
+
)
|
|
105
|
+
await asyncio.sleep(retry_delay)
|
|
106
|
+
else:
|
|
107
|
+
logger.error(
|
|
108
|
+
f"Failed to create OSO Cloud client after {max_retries} attempts: {e}",
|
|
109
|
+
exc_info=True,
|
|
110
|
+
)
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
# Should never reach here, but just in case
|
|
114
|
+
if last_error:
|
|
115
|
+
raise last_error
|
|
116
|
+
raise RuntimeError("Failed to create OSO Cloud client")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def setup_initial_oso_facts(
|
|
120
|
+
authz_provider: OsoAdapter,
|
|
121
|
+
initial_roles: Optional[List[Dict[str, Any]]] = None,
|
|
122
|
+
initial_policies: Optional[List[Dict[str, Any]]] = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Set up initial roles and policies in OSO Cloud.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
authz_provider: OsoAdapter instance
|
|
129
|
+
initial_roles: List of role assignments, e.g.:
|
|
130
|
+
[{"user": "alice@example.com", "role": "admin", "resource": "documents"}]
|
|
131
|
+
If "resource" is not specified, defaults to "documents" for backward compatibility
|
|
132
|
+
initial_policies: List of permission policies, e.g.:
|
|
133
|
+
[{"role": "admin", "resource": "documents", "action": "read"}]
|
|
134
|
+
"""
|
|
135
|
+
if initial_roles:
|
|
136
|
+
for role_assignment in initial_roles:
|
|
137
|
+
try:
|
|
138
|
+
user = role_assignment.get("user")
|
|
139
|
+
role = role_assignment.get("role")
|
|
140
|
+
resource = role_assignment.get(
|
|
141
|
+
"resource", "documents"
|
|
142
|
+
) # Default to "documents"
|
|
143
|
+
|
|
144
|
+
if user and role:
|
|
145
|
+
# For OSO Cloud, we add has_role facts with resource context
|
|
146
|
+
# This supports resource-based authorization
|
|
147
|
+
await authz_provider.add_role_for_user(user, role, resource)
|
|
148
|
+
logger.debug(
|
|
149
|
+
f"Added role '{role}' for user '{user}' on resource '{resource}'"
|
|
150
|
+
)
|
|
151
|
+
except (ValueError, TypeError, AttributeError, RuntimeError) as e:
|
|
152
|
+
logger.warning(
|
|
153
|
+
f"Failed to add initial role assignment {role_assignment}: {e}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Note: initial_policies are not used - we use has_role facts instead
|
|
157
|
+
# The policy derives permissions from roles, not from explicit grants_permission facts
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def initialize_oso_from_manifest(
|
|
161
|
+
engine, app_slug: str, auth_config: Dict[str, Any]
|
|
162
|
+
) -> Optional[OsoAdapter]:
|
|
163
|
+
"""
|
|
164
|
+
Initialize OSO Cloud provider from manifest configuration.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
engine: MongoDBEngine instance
|
|
168
|
+
app_slug: App slug identifier
|
|
169
|
+
auth_config: Auth configuration dict from manifest (contains auth_policy)
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
OsoAdapter instance if successfully created, None otherwise
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
from .provider import OsoAdapter
|
|
176
|
+
|
|
177
|
+
# Support both old (auth_policy) and new (auth.policy) structures
|
|
178
|
+
auth = auth_config.get("auth", {})
|
|
179
|
+
auth_policy = auth.get("policy", {}) or auth_config.get("auth_policy", {})
|
|
180
|
+
provider = auth_policy.get("provider", "casbin")
|
|
181
|
+
|
|
182
|
+
# Only proceed if provider is oso
|
|
183
|
+
if provider != "oso":
|
|
184
|
+
logger.debug(
|
|
185
|
+
f"Provider is '{provider}', not 'oso' - skipping OSO initialization"
|
|
186
|
+
)
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
logger.info(f"Initializing OSO Cloud provider for app '{app_slug}'...")
|
|
190
|
+
|
|
191
|
+
# Get authorization config
|
|
192
|
+
authorization = auth_policy.get("authorization", {})
|
|
193
|
+
|
|
194
|
+
# Get API key from config or environment
|
|
195
|
+
api_key = authorization.get("api_key") or os.getenv("OSO_AUTH")
|
|
196
|
+
url = authorization.get("url") or os.getenv("OSO_URL")
|
|
197
|
+
|
|
198
|
+
logger.debug(
|
|
199
|
+
f"OSO config - API key: {'***' if api_key else 'NOT SET'}, "
|
|
200
|
+
f"URL: {url or 'NOT SET (using default)'}"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if not api_key:
|
|
204
|
+
logger.error(
|
|
205
|
+
f"❌ OSO Cloud API key not found for app '{app_slug}'. "
|
|
206
|
+
"Set OSO_AUTH environment variable or provide api_key in manifest."
|
|
207
|
+
)
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
# Create OSO Cloud client
|
|
211
|
+
logger.debug("Creating OSO Cloud client...")
|
|
212
|
+
try:
|
|
213
|
+
# More retries for Dev Server which might take time to start
|
|
214
|
+
oso_client = await create_oso_cloud_client(
|
|
215
|
+
api_key=api_key,
|
|
216
|
+
url=url,
|
|
217
|
+
max_retries=5,
|
|
218
|
+
retry_delay=3.0, # 3 second delay between retries
|
|
219
|
+
)
|
|
220
|
+
logger.info("✅ OSO Cloud client created successfully")
|
|
221
|
+
|
|
222
|
+
# Test connection by trying a simple authorize call
|
|
223
|
+
# This will fail if the Dev Server isn't ready or policy isn't loaded
|
|
224
|
+
try:
|
|
225
|
+
import asyncio
|
|
226
|
+
|
|
227
|
+
from oso_cloud import Value
|
|
228
|
+
|
|
229
|
+
# Try a simple test authorization to verify connection
|
|
230
|
+
test_actor = Value("User", "test")
|
|
231
|
+
test_resource = Value("Document", "test")
|
|
232
|
+
# This might fail, but it tests if the server is responding
|
|
233
|
+
await asyncio.to_thread(
|
|
234
|
+
oso_client.authorize, test_actor, "read", test_resource
|
|
235
|
+
)
|
|
236
|
+
logger.debug("✅ OSO Dev Server connection test successful")
|
|
237
|
+
except (TimeoutError, OSError, RuntimeError) as test_error:
|
|
238
|
+
# Type 2: Recoverable - connection test failed, check if it's a connection error
|
|
239
|
+
error_str = str(test_error).lower()
|
|
240
|
+
if (
|
|
241
|
+
"connection" in error_str
|
|
242
|
+
or "refused" in error_str
|
|
243
|
+
or "timeout" in error_str
|
|
244
|
+
):
|
|
245
|
+
logger.warning(
|
|
246
|
+
f"⚠️ OSO Dev Server connection test failed - "
|
|
247
|
+
f"server might not be ready: {test_error}"
|
|
248
|
+
)
|
|
249
|
+
logger.warning(
|
|
250
|
+
" This is OK if the Dev Server is still starting up. "
|
|
251
|
+
"Will retry on first real request."
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
# Other errors (like authorization denied) are expected and OK
|
|
255
|
+
logger.debug(
|
|
256
|
+
f"OSO connection test completed "
|
|
257
|
+
f"(authorization denied is expected): {test_error}"
|
|
258
|
+
)
|
|
259
|
+
except (
|
|
260
|
+
ImportError,
|
|
261
|
+
AttributeError,
|
|
262
|
+
TypeError,
|
|
263
|
+
ValueError,
|
|
264
|
+
RuntimeError,
|
|
265
|
+
ConnectionError,
|
|
266
|
+
) as e:
|
|
267
|
+
logger.error(f"❌ Failed to create OSO Cloud client: {e}", exc_info=True)
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
# Create adapter
|
|
271
|
+
logger.debug("Creating OsoAdapter...")
|
|
272
|
+
adapter = OsoAdapter(oso_client)
|
|
273
|
+
logger.info("✅ OsoAdapter created successfully")
|
|
274
|
+
|
|
275
|
+
# Set up initial facts if configured
|
|
276
|
+
initial_roles = authorization.get("initial_roles", [])
|
|
277
|
+
initial_policies = authorization.get("initial_policies", [])
|
|
278
|
+
|
|
279
|
+
if initial_roles or initial_policies:
|
|
280
|
+
logger.info(
|
|
281
|
+
f"Setting up initial OSO facts: {len(initial_roles)} roles, "
|
|
282
|
+
f"{len(initial_policies)} policies"
|
|
283
|
+
)
|
|
284
|
+
try:
|
|
285
|
+
await setup_initial_oso_facts(
|
|
286
|
+
adapter,
|
|
287
|
+
initial_roles=initial_roles,
|
|
288
|
+
initial_policies=initial_policies,
|
|
289
|
+
)
|
|
290
|
+
logger.info("✅ Initial OSO facts set up successfully")
|
|
291
|
+
except (ValueError, TypeError, AttributeError, RuntimeError) as e:
|
|
292
|
+
logger.warning(
|
|
293
|
+
f"⚠️ Failed to set up initial OSO facts: {e}", exc_info=True
|
|
294
|
+
)
|
|
295
|
+
# Continue anyway - adapter is still usable
|
|
296
|
+
|
|
297
|
+
logger.info(f"✅ OSO Cloud provider initialized for app '{app_slug}'")
|
|
298
|
+
|
|
299
|
+
return adapter
|
|
300
|
+
|
|
301
|
+
except ImportError as e:
|
|
302
|
+
logger.error(
|
|
303
|
+
f"❌ OSO Cloud SDK not available for app '{app_slug}': {e}. "
|
|
304
|
+
"Install with: pip install oso-cloud"
|
|
305
|
+
)
|
|
306
|
+
return None
|
|
307
|
+
except ValueError as e:
|
|
308
|
+
logger.error(f"❌ OSO Cloud configuration error for app '{app_slug}': {e}")
|
|
309
|
+
return None
|
|
310
|
+
except (
|
|
311
|
+
ImportError,
|
|
312
|
+
AttributeError,
|
|
313
|
+
TypeError,
|
|
314
|
+
ValueError,
|
|
315
|
+
RuntimeError,
|
|
316
|
+
ConnectionError,
|
|
317
|
+
KeyError,
|
|
318
|
+
) as e:
|
|
319
|
+
logger.error(
|
|
320
|
+
f"❌ Error initializing OSO Cloud provider for app '{app_slug}': {e}",
|
|
321
|
+
exc_info=True,
|
|
322
|
+
)
|
|
323
|
+
return None
|