mdb-engine 0.1.6__py3-none-any.whl → 0.4.12__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 +116 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +654 -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 +265 -70
- mdb_engine/auth/config_defaults.py +5 -5
- mdb_engine/auth/config_helpers.py +19 -18
- mdb_engine/auth/cookie_utils.py +12 -16
- mdb_engine/auth/csrf.py +483 -0
- mdb_engine/auth/decorators.py +10 -16
- mdb_engine/auth/dependencies.py +69 -71
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +61 -88
- mdb_engine/auth/jwt.py +11 -15
- mdb_engine/auth/middleware.py +79 -35
- mdb_engine/auth/oso_factory.py +21 -41
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +505 -0
- mdb_engine/auth/restrictions.py +21 -36
- mdb_engine/auth/session_manager.py +24 -41
- mdb_engine/auth/shared_middleware.py +977 -0
- mdb_engine/auth/shared_users.py +775 -0
- mdb_engine/auth/token_lifecycle.py +10 -12
- mdb_engine/auth/token_store.py +17 -32
- mdb_engine/auth/users.py +99 -159
- mdb_engine/auth/utils.py +236 -42
- mdb_engine/cli/commands/generate.py +546 -10
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +7 -7
- mdb_engine/config.py +13 -28
- 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 +31 -50
- mdb_engine/core/app_secrets.py +289 -0
- mdb_engine/core/connection.py +20 -12
- mdb_engine/core/encryption.py +222 -0
- mdb_engine/core/engine.py +2862 -115
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +628 -204
- mdb_engine/core/ray_integration.py +436 -0
- mdb_engine/core/seeding.py +13 -21
- mdb_engine/core/service_initialization.py +20 -30
- mdb_engine/core/types.py +40 -43
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +37 -50
- mdb_engine/database/connection.py +51 -30
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +747 -237
- mdb_engine/dependencies.py +427 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +247 -0
- mdb_engine/di/providers.py +206 -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 +38 -155
- mdb_engine/embeddings/service.py +78 -75
- mdb_engine/exceptions.py +104 -12
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +11 -11
- mdb_engine/indexes/manager.py +59 -123
- mdb_engine/memory/README.md +95 -4
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +363 -1168
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +17 -17
- mdb_engine/observability/logging.py +10 -10
- mdb_engine/observability/metrics.py +40 -19
- 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 +41 -75
- mdb_engine/utils/__init__.py +3 -1
- mdb_engine/utils/mongo.py +117 -0
- mdb_engine-0.4.12.dist-info/METADATA +492 -0
- mdb_engine-0.4.12.dist-info/RECORD +97 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
- 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.4.12.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
mdb_engine/auth/jwt.py
CHANGED
|
@@ -10,7 +10,7 @@ This module is part of MDB_ENGINE - MongoDB Engine.
|
|
|
10
10
|
import logging
|
|
11
11
|
import uuid
|
|
12
12
|
from datetime import datetime, timedelta
|
|
13
|
-
from typing import Any
|
|
13
|
+
from typing import Any
|
|
14
14
|
|
|
15
15
|
import jwt
|
|
16
16
|
|
|
@@ -21,7 +21,7 @@ from ..constants import CURRENT_TOKEN_VERSION
|
|
|
21
21
|
logger = logging.getLogger(__name__)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def decode_jwt_token(token: Any, secret_key: str) ->
|
|
24
|
+
def decode_jwt_token(token: Any, secret_key: str) -> dict[str, Any]:
|
|
25
25
|
"""
|
|
26
26
|
Helper function to decode JWT tokens with automatic fallback to bytes format.
|
|
27
27
|
|
|
@@ -74,7 +74,7 @@ def decode_jwt_token(token: Any, secret_key: str) -> Dict[str, Any]:
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def encode_jwt_token(
|
|
77
|
-
payload:
|
|
77
|
+
payload: dict[str, Any], secret_key: str, expires_in: int | None = None
|
|
78
78
|
) -> str:
|
|
79
79
|
"""
|
|
80
80
|
Encode a JWT token with enhanced claims.
|
|
@@ -123,12 +123,12 @@ def encode_jwt_token(
|
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def generate_token_pair(
|
|
126
|
-
user_data:
|
|
126
|
+
user_data: dict[str, Any],
|
|
127
127
|
secret_key: str,
|
|
128
|
-
device_info:
|
|
129
|
-
access_token_ttl:
|
|
130
|
-
refresh_token_ttl:
|
|
131
|
-
) ->
|
|
128
|
+
device_info: dict[str, Any] | None = None,
|
|
129
|
+
access_token_ttl: int | None = None,
|
|
130
|
+
refresh_token_ttl: int | None = None,
|
|
131
|
+
) -> tuple[str, str, dict[str, Any]]:
|
|
132
132
|
"""
|
|
133
133
|
Generate a pair of access and refresh tokens.
|
|
134
134
|
|
|
@@ -164,9 +164,7 @@ def generate_token_pair(
|
|
|
164
164
|
"jti": access_jti,
|
|
165
165
|
"device_id": device_id,
|
|
166
166
|
}
|
|
167
|
-
access_token = encode_jwt_token(
|
|
168
|
-
access_payload, secret_key, expires_in=access_token_ttl
|
|
169
|
-
)
|
|
167
|
+
access_token = encode_jwt_token(access_payload, secret_key, expires_in=access_token_ttl)
|
|
170
168
|
|
|
171
169
|
# Generate refresh token
|
|
172
170
|
refresh_jti = str(uuid.uuid4())
|
|
@@ -177,9 +175,7 @@ def generate_token_pair(
|
|
|
177
175
|
"email": user_data.get("email"),
|
|
178
176
|
"device_id": device_id,
|
|
179
177
|
}
|
|
180
|
-
refresh_token = encode_jwt_token(
|
|
181
|
-
refresh_payload, secret_key, expires_in=refresh_token_ttl
|
|
182
|
-
)
|
|
178
|
+
refresh_token = encode_jwt_token(refresh_payload, secret_key, expires_in=refresh_token_ttl)
|
|
183
179
|
|
|
184
180
|
# Token metadata
|
|
185
181
|
token_metadata = {
|
|
@@ -194,7 +190,7 @@ def generate_token_pair(
|
|
|
194
190
|
return access_token, refresh_token, token_metadata
|
|
195
191
|
|
|
196
192
|
|
|
197
|
-
def extract_token_metadata(token: str, secret_key: str) ->
|
|
193
|
+
def extract_token_metadata(token: str, secret_key: str) -> dict[str, Any] | None:
|
|
198
194
|
"""
|
|
199
195
|
Extract metadata from a token without full validation.
|
|
200
196
|
|
mdb_engine/auth/middleware.py
CHANGED
|
@@ -4,12 +4,19 @@ Security Middleware
|
|
|
4
4
|
Middleware for enforcing security settings from manifest configuration.
|
|
5
5
|
|
|
6
6
|
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
7
|
+
|
|
8
|
+
Security Features:
|
|
9
|
+
- HTTPS enforcement in production
|
|
10
|
+
- HSTS (HTTP Strict Transport Security) header
|
|
11
|
+
- Security headers (X-Content-Type-Options, X-Frame-Options, etc.)
|
|
12
|
+
- CSRF token generation (legacy, prefer CSRFMiddleware for new apps)
|
|
7
13
|
"""
|
|
8
14
|
|
|
9
15
|
import logging
|
|
10
16
|
import os
|
|
11
17
|
import secrets
|
|
12
|
-
from
|
|
18
|
+
from collections.abc import Awaitable, Callable
|
|
19
|
+
from typing import Any
|
|
13
20
|
|
|
14
21
|
from fastapi import HTTPException, Request, Response, status
|
|
15
22
|
from fastapi.responses import RedirectResponse
|
|
@@ -17,6 +24,18 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
17
24
|
|
|
18
25
|
logger = logging.getLogger(__name__)
|
|
19
26
|
|
|
27
|
+
# Default HSTS settings
|
|
28
|
+
DEFAULT_HSTS_MAX_AGE = 31536000 # 1 year in seconds
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_production() -> bool:
|
|
32
|
+
"""Check if we're running in production environment."""
|
|
33
|
+
return (
|
|
34
|
+
os.getenv("MDB_ENGINE_ENV", "").lower() == "production"
|
|
35
|
+
or os.getenv("ENVIRONMENT", "").lower() == "production"
|
|
36
|
+
or os.getenv("G_NOME_ENV", "").lower() == "production"
|
|
37
|
+
)
|
|
38
|
+
|
|
20
39
|
|
|
21
40
|
class SecurityMiddleware(BaseHTTPMiddleware):
|
|
22
41
|
"""
|
|
@@ -24,9 +43,9 @@ class SecurityMiddleware(BaseHTTPMiddleware):
|
|
|
24
43
|
|
|
25
44
|
Features:
|
|
26
45
|
- HTTPS enforcement in production
|
|
27
|
-
-
|
|
28
|
-
- Security headers
|
|
29
|
-
-
|
|
46
|
+
- HSTS header for forcing HTTPS
|
|
47
|
+
- Security headers (X-Content-Type-Options, X-Frame-Options, etc.)
|
|
48
|
+
- Legacy CSRF token generation (prefer CSRFMiddleware for new apps)
|
|
30
49
|
"""
|
|
31
50
|
|
|
32
51
|
def __init__(
|
|
@@ -35,6 +54,7 @@ class SecurityMiddleware(BaseHTTPMiddleware):
|
|
|
35
54
|
require_https: bool = False,
|
|
36
55
|
csrf_protection: bool = True,
|
|
37
56
|
security_headers: bool = True,
|
|
57
|
+
hsts_config: dict[str, Any] | None = None,
|
|
38
58
|
):
|
|
39
59
|
"""
|
|
40
60
|
Initialize security middleware.
|
|
@@ -42,38 +62,60 @@ class SecurityMiddleware(BaseHTTPMiddleware):
|
|
|
42
62
|
Args:
|
|
43
63
|
app: FastAPI application
|
|
44
64
|
require_https: Require HTTPS in production (default: False, auto-detected)
|
|
45
|
-
csrf_protection: Enable CSRF protection (default: True)
|
|
65
|
+
csrf_protection: Enable legacy CSRF protection (default: True)
|
|
46
66
|
security_headers: Add security headers (default: True)
|
|
67
|
+
hsts_config: HSTS configuration dict with keys:
|
|
68
|
+
- enabled: Enable HSTS (default: True in production)
|
|
69
|
+
- max_age: Max-age in seconds (default: 31536000)
|
|
70
|
+
- include_subdomains: Include subdomains (default: True)
|
|
71
|
+
- preload: Add preload directive (default: False)
|
|
47
72
|
"""
|
|
48
73
|
super().__init__(app)
|
|
49
74
|
self.require_https = require_https
|
|
50
75
|
self.csrf_protection = csrf_protection
|
|
51
76
|
self.security_headers = security_headers
|
|
52
77
|
|
|
78
|
+
# HSTS configuration
|
|
79
|
+
self.hsts_config = hsts_config or {}
|
|
80
|
+
self.hsts_enabled = self.hsts_config.get("enabled", True)
|
|
81
|
+
self.hsts_max_age = self.hsts_config.get("max_age", DEFAULT_HSTS_MAX_AGE)
|
|
82
|
+
self.hsts_include_subdomains = self.hsts_config.get("include_subdomains", True)
|
|
83
|
+
self.hsts_preload = self.hsts_config.get("preload", False)
|
|
84
|
+
|
|
85
|
+
def _build_hsts_header(self) -> str:
|
|
86
|
+
"""Build the HSTS header value."""
|
|
87
|
+
parts = [f"max-age={self.hsts_max_age}"]
|
|
88
|
+
|
|
89
|
+
if self.hsts_include_subdomains:
|
|
90
|
+
parts.append("includeSubDomains")
|
|
91
|
+
|
|
92
|
+
if self.hsts_preload:
|
|
93
|
+
parts.append("preload")
|
|
94
|
+
|
|
95
|
+
return "; ".join(parts)
|
|
96
|
+
|
|
53
97
|
async def dispatch(
|
|
54
98
|
self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
|
55
99
|
) -> Response:
|
|
56
100
|
"""
|
|
57
101
|
Process request through security middleware.
|
|
58
102
|
"""
|
|
103
|
+
is_production = _is_production()
|
|
104
|
+
is_https = request.url.scheme == "https"
|
|
105
|
+
|
|
59
106
|
# Check HTTPS requirement
|
|
60
|
-
if self.require_https:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
status_code=status.HTTP_403_FORBIDDEN,
|
|
73
|
-
detail="HTTPS required in production",
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
# Generate CSRF token if not present (for GET requests)
|
|
107
|
+
if self.require_https and is_production and not is_https:
|
|
108
|
+
if request.method == "GET":
|
|
109
|
+
# Redirect to HTTPS
|
|
110
|
+
https_url = str(request.url).replace("http://", "https://", 1)
|
|
111
|
+
return RedirectResponse(url=https_url, status_code=301)
|
|
112
|
+
else:
|
|
113
|
+
raise HTTPException(
|
|
114
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
115
|
+
detail="HTTPS required in production",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Generate CSRF token if not present (for GET requests) - legacy support
|
|
77
119
|
if self.csrf_protection and request.method == "GET":
|
|
78
120
|
csrf_token = request.cookies.get("csrf_token")
|
|
79
121
|
if not csrf_token:
|
|
@@ -90,19 +132,27 @@ class SecurityMiddleware(BaseHTTPMiddleware):
|
|
|
90
132
|
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
91
133
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
92
134
|
|
|
135
|
+
# Permissions-Policy (modern replacement for some legacy headers)
|
|
136
|
+
response.headers["Permissions-Policy"] = (
|
|
137
|
+
"accelerometer=(), camera=(), geolocation=(), gyroscope=(), "
|
|
138
|
+
"magnetometer=(), microphone=(), payment=(), usb=()"
|
|
139
|
+
)
|
|
140
|
+
|
|
93
141
|
# Content Security Policy (basic)
|
|
94
142
|
if request.url.path.startswith("/api"):
|
|
95
143
|
response.headers["Content-Security-Policy"] = "default-src 'self'"
|
|
96
144
|
|
|
97
|
-
#
|
|
145
|
+
# Add HSTS header in production (only over HTTPS or always if configured)
|
|
146
|
+
if self.hsts_enabled and (is_production or is_https):
|
|
147
|
+
response.headers["Strict-Transport-Security"] = self._build_hsts_header()
|
|
148
|
+
|
|
149
|
+
# Set CSRF token cookie if generated (legacy support)
|
|
98
150
|
if (
|
|
99
151
|
self.csrf_protection
|
|
100
152
|
and request.method == "GET"
|
|
101
153
|
and not request.cookies.get("csrf_token")
|
|
102
154
|
):
|
|
103
155
|
csrf_token = secrets.token_urlsafe(32)
|
|
104
|
-
is_https = request.url.scheme == "https"
|
|
105
|
-
is_production = os.getenv("G_NOME_ENV") == "production"
|
|
106
156
|
response.set_cookie(
|
|
107
157
|
key="csrf_token",
|
|
108
158
|
value=csrf_token,
|
|
@@ -154,10 +204,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
|
|
|
154
204
|
# Check if we need to clear a stale session cookie
|
|
155
205
|
# Only act if explicitly flagged - this ensures we don't interfere with
|
|
156
206
|
# apps that don't use get_app_user()
|
|
157
|
-
if (
|
|
158
|
-
hasattr(request.state, "clear_stale_session")
|
|
159
|
-
and request.state.clear_stale_session
|
|
160
|
-
):
|
|
207
|
+
if hasattr(request.state, "clear_stale_session") and request.state.clear_stale_session:
|
|
161
208
|
try:
|
|
162
209
|
# Get cookie name from app config
|
|
163
210
|
cookie_name = None
|
|
@@ -188,7 +235,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
|
|
|
188
235
|
"session_cookie_name", "app_session"
|
|
189
236
|
)
|
|
190
237
|
cookie_name = f"{session_cookie_name}_{self.slug_id}"
|
|
191
|
-
except (AttributeError, KeyError, TypeError
|
|
238
|
+
except (AttributeError, KeyError, TypeError):
|
|
192
239
|
pass
|
|
193
240
|
|
|
194
241
|
# Final fallback to default naming convention
|
|
@@ -197,8 +244,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
|
|
|
197
244
|
|
|
198
245
|
# Get cookie settings to match how it was set
|
|
199
246
|
should_use_secure = (
|
|
200
|
-
request.url.scheme == "https"
|
|
201
|
-
or os.getenv("G_NOME_ENV") == "production"
|
|
247
|
+
request.url.scheme == "https" or os.getenv("G_NOME_ENV") == "production"
|
|
202
248
|
)
|
|
203
249
|
|
|
204
250
|
# Delete the stale cookie
|
|
@@ -208,9 +254,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
|
|
|
208
254
|
secure=should_use_secure,
|
|
209
255
|
samesite="lax",
|
|
210
256
|
)
|
|
211
|
-
logger.debug(
|
|
212
|
-
f"Cleared stale session cookie '{cookie_name}' for {self.slug_id}"
|
|
213
|
-
)
|
|
257
|
+
logger.debug(f"Cleared stale session cookie '{cookie_name}' for {self.slug_id}")
|
|
214
258
|
except (ValueError, TypeError, AttributeError, RuntimeError) as e:
|
|
215
259
|
# Don't fail the request if cookie cleanup fails
|
|
216
260
|
logger.warning(
|
mdb_engine/auth/oso_factory.py
CHANGED
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
13
|
import os
|
|
14
|
-
from typing import TYPE_CHECKING, Any
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from .provider import OsoAdapter
|
|
@@ -20,8 +20,8 @@ logger = logging.getLogger(__name__)
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
async def create_oso_cloud_client(
|
|
23
|
-
api_key:
|
|
24
|
-
url:
|
|
23
|
+
api_key: str | None = None,
|
|
24
|
+
url: str | None = None,
|
|
25
25
|
max_retries: int = 3,
|
|
26
26
|
retry_delay: float = 2.0,
|
|
27
27
|
) -> Any:
|
|
@@ -46,13 +46,11 @@ async def create_oso_cloud_client(
|
|
|
46
46
|
|
|
47
47
|
# Import OSO Cloud SDK - the class is named "Oso"
|
|
48
48
|
try:
|
|
49
|
-
from oso_cloud import Oso
|
|
49
|
+
from oso_cloud import Oso # type: ignore
|
|
50
50
|
|
|
51
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
|
-
)
|
|
52
|
+
except ImportError as e:
|
|
53
|
+
raise ImportError("OSO Cloud SDK not installed. Install with: pip install oso-cloud") from e
|
|
56
54
|
|
|
57
55
|
# Get API key from parameter or environment
|
|
58
56
|
if not api_key:
|
|
@@ -81,9 +79,7 @@ async def create_oso_cloud_client(
|
|
|
81
79
|
|
|
82
80
|
# Note: OSO client creation doesn't actually connect to the server
|
|
83
81
|
# 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
|
-
)
|
|
82
|
+
logger.info(f"✅ OSO Cloud client created successfully (URL: {url or 'default'})")
|
|
87
83
|
if url:
|
|
88
84
|
logger.info(f" Using OSO Dev Server at: {url}")
|
|
89
85
|
return oso_client
|
|
@@ -118,8 +114,8 @@ async def create_oso_cloud_client(
|
|
|
118
114
|
|
|
119
115
|
async def setup_initial_oso_facts(
|
|
120
116
|
authz_provider: OsoAdapter,
|
|
121
|
-
initial_roles:
|
|
122
|
-
initial_policies:
|
|
117
|
+
initial_roles: list[dict[str, Any]] | None = None,
|
|
118
|
+
initial_policies: list[dict[str, Any]] | None = None,
|
|
123
119
|
) -> None:
|
|
124
120
|
"""
|
|
125
121
|
Set up initial roles and policies in OSO Cloud.
|
|
@@ -137,29 +133,23 @@ async def setup_initial_oso_facts(
|
|
|
137
133
|
try:
|
|
138
134
|
user = role_assignment.get("user")
|
|
139
135
|
role = role_assignment.get("role")
|
|
140
|
-
resource = role_assignment.get(
|
|
141
|
-
"resource", "documents"
|
|
142
|
-
) # Default to "documents"
|
|
136
|
+
resource = role_assignment.get("resource", "documents") # Default to "documents"
|
|
143
137
|
|
|
144
138
|
if user and role:
|
|
145
139
|
# For OSO Cloud, we add has_role facts with resource context
|
|
146
140
|
# This supports resource-based authorization
|
|
147
141
|
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
|
-
)
|
|
142
|
+
logger.debug(f"Added role '{role}' for user '{user}' on resource '{resource}'")
|
|
151
143
|
except (ValueError, TypeError, AttributeError, RuntimeError) as e:
|
|
152
|
-
logger.warning(
|
|
153
|
-
f"Failed to add initial role assignment {role_assignment}: {e}"
|
|
154
|
-
)
|
|
144
|
+
logger.warning(f"Failed to add initial role assignment {role_assignment}: {e}")
|
|
155
145
|
|
|
156
146
|
# Note: initial_policies are not used - we use has_role facts instead
|
|
157
147
|
# The policy derives permissions from roles, not from explicit grants_permission facts
|
|
158
148
|
|
|
159
149
|
|
|
160
150
|
async def initialize_oso_from_manifest(
|
|
161
|
-
engine, app_slug: str, auth_config:
|
|
162
|
-
) ->
|
|
151
|
+
engine, app_slug: str, auth_config: dict[str, Any]
|
|
152
|
+
) -> OsoAdapter | None:
|
|
163
153
|
"""
|
|
164
154
|
Initialize OSO Cloud provider from manifest configuration.
|
|
165
155
|
|
|
@@ -181,9 +171,7 @@ async def initialize_oso_from_manifest(
|
|
|
181
171
|
|
|
182
172
|
# Only proceed if provider is oso
|
|
183
173
|
if provider != "oso":
|
|
184
|
-
logger.debug(
|
|
185
|
-
f"Provider is '{provider}', not 'oso' - skipping OSO initialization"
|
|
186
|
-
)
|
|
174
|
+
logger.debug(f"Provider is '{provider}', not 'oso' - skipping OSO initialization")
|
|
187
175
|
return None
|
|
188
176
|
|
|
189
177
|
logger.info(f"Initializing OSO Cloud provider for app '{app_slug}'...")
|
|
@@ -224,24 +212,18 @@ async def initialize_oso_from_manifest(
|
|
|
224
212
|
try:
|
|
225
213
|
import asyncio
|
|
226
214
|
|
|
227
|
-
from oso_cloud import Value
|
|
215
|
+
from oso_cloud import Value # type: ignore
|
|
228
216
|
|
|
229
217
|
# Try a simple test authorization to verify connection
|
|
230
218
|
test_actor = Value("User", "test")
|
|
231
219
|
test_resource = Value("Document", "test")
|
|
232
220
|
# 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
|
-
)
|
|
221
|
+
await asyncio.to_thread(oso_client.authorize, test_actor, "read", test_resource)
|
|
236
222
|
logger.debug("✅ OSO Dev Server connection test successful")
|
|
237
223
|
except (TimeoutError, OSError, RuntimeError) as test_error:
|
|
238
224
|
# Type 2: Recoverable - connection test failed, check if it's a connection error
|
|
239
225
|
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
|
-
):
|
|
226
|
+
if "connection" in error_str or "refused" in error_str or "timeout" in error_str:
|
|
245
227
|
logger.warning(
|
|
246
228
|
f"⚠️ OSO Dev Server connection test failed - "
|
|
247
229
|
f"server might not be ready: {test_error}"
|
|
@@ -289,9 +271,7 @@ async def initialize_oso_from_manifest(
|
|
|
289
271
|
)
|
|
290
272
|
logger.info("✅ Initial OSO facts set up successfully")
|
|
291
273
|
except (ValueError, TypeError, AttributeError, RuntimeError) as e:
|
|
292
|
-
logger.warning(
|
|
293
|
-
f"⚠️ Failed to set up initial OSO facts: {e}", exc_info=True
|
|
294
|
-
)
|
|
274
|
+
logger.warning(f"⚠️ Failed to set up initial OSO facts: {e}", exc_info=True)
|
|
295
275
|
# Continue anyway - adapter is still usable
|
|
296
276
|
|
|
297
277
|
logger.info(f"✅ OSO Cloud provider initialized for app '{app_slug}'")
|
|
@@ -299,13 +279,13 @@ async def initialize_oso_from_manifest(
|
|
|
299
279
|
return adapter
|
|
300
280
|
|
|
301
281
|
except ImportError as e:
|
|
302
|
-
logger.
|
|
282
|
+
logger.exception(
|
|
303
283
|
f"❌ OSO Cloud SDK not available for app '{app_slug}': {e}. "
|
|
304
284
|
"Install with: pip install oso-cloud"
|
|
305
285
|
)
|
|
306
286
|
return None
|
|
307
287
|
except ValueError as e:
|
|
308
|
-
logger.
|
|
288
|
+
logger.exception(f"❌ OSO Cloud configuration error for app '{app_slug}': {e}")
|
|
309
289
|
return None
|
|
310
290
|
except (
|
|
311
291
|
ImportError,
|