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 CHANGED
@@ -1,8 +1,11 @@
1
1
  """
2
2
  MDB_ENGINE - MongoDB Engine
3
3
 
4
- Enterprise-grade engine for building applications
5
- with automatic database scoping, authentication, and resource management.
4
+ Enterprise-grade engine for building applications with:
5
+ - Automatic database scoping and data isolation
6
+ - Proper dependency injection with service lifetimes
7
+ - Repository pattern for clean data access
8
+ - Authentication and authorization
6
9
 
7
10
  Usage:
8
11
  # Simple usage
@@ -14,14 +17,19 @@ Usage:
14
17
  # With FastAPI integration
15
18
  app = engine.create_app(slug="my_app", manifest=Path("manifest.json"))
16
19
 
17
- # With Ray support (optional)
18
- engine = MongoDBEngine(..., enable_ray=True)
20
+ # In routes - use RequestContext for clean DI
21
+ from mdb_engine import RequestContext
22
+
23
+ @app.get("/users/{user_id}")
24
+ async def get_user(user_id: str, ctx: RequestContext = Depends()):
25
+ user = await ctx.uow.users.get(user_id)
26
+ return user
19
27
  """
20
28
 
21
29
  # Authentication
22
- from .auth import AuthorizationProvider, get_current_user, require_admin
30
+ from .auth import AuthorizationProvider, require_admin
31
+ from .auth import get_current_user as auth_get_current_user # noqa: F401
23
32
 
24
- # Optional Ray integration
25
33
  # Core MongoDB Engine
26
34
  from .core import (
27
35
  RAY_AVAILABLE,
@@ -36,6 +44,30 @@ from .core import (
36
44
  # Database layer
37
45
  from .database import AppDB, ScopedMongoWrapper
38
46
 
47
+ # FastAPI dependencies
48
+ from .dependencies import (
49
+ Inject,
50
+ RequestContext,
51
+ get_app_config,
52
+ get_app_slug,
53
+ get_authz_provider,
54
+ get_current_user,
55
+ get_embedding_service,
56
+ get_engine,
57
+ get_llm_client,
58
+ get_llm_model_name,
59
+ get_memory_service,
60
+ get_scoped_db,
61
+ get_unit_of_work,
62
+ get_user_roles,
63
+ inject,
64
+ require_role,
65
+ require_user,
66
+ )
67
+
68
+ # DI Container
69
+ from .di import Container, Scope, ScopeManager
70
+
39
71
  # Index management
40
72
  from .indexes import (
41
73
  AsyncAtlasIndexManager,
@@ -43,14 +75,17 @@ from .indexes import (
43
75
  run_index_creation_for_collection,
44
76
  )
45
77
 
46
- __version__ = "0.1.6"
78
+ # Repository pattern
79
+ from .repositories import Entity, MongoRepository, Repository, UnitOfWork
80
+
81
+ __version__ = "0.2.0" # Major version bump for new DI system
47
82
 
48
83
  __all__ = [
49
- # Core (includes FastAPI integration and optional Ray)
84
+ # Core Engine
50
85
  "MongoDBEngine",
51
86
  "ManifestValidator",
52
87
  "ManifestParser",
53
- # Ray Integration (optional - only active if Ray installed)
88
+ # Ray Integration (optional)
54
89
  "RAY_AVAILABLE",
55
90
  "AppRayActor",
56
91
  "get_ray_actor_handle",
@@ -58,10 +93,36 @@ __all__ = [
58
93
  # Database
59
94
  "ScopedMongoWrapper",
60
95
  "AppDB",
96
+ # DI Container
97
+ "Container",
98
+ "Scope",
99
+ "ScopeManager",
100
+ # Repository Pattern
101
+ "Repository",
102
+ "MongoRepository",
103
+ "Entity",
104
+ "UnitOfWork",
61
105
  # Auth
62
106
  "AuthorizationProvider",
63
- "get_current_user",
64
107
  "require_admin",
108
+ # FastAPI Dependencies
109
+ "RequestContext",
110
+ "get_engine",
111
+ "get_app_slug",
112
+ "get_app_config",
113
+ "get_scoped_db",
114
+ "get_unit_of_work",
115
+ "get_embedding_service",
116
+ "get_memory_service",
117
+ "get_llm_client",
118
+ "get_llm_model_name",
119
+ "get_authz_provider",
120
+ "get_current_user",
121
+ "get_user_roles",
122
+ "require_user",
123
+ "require_role",
124
+ "inject",
125
+ "Inject",
65
126
  # Indexes
66
127
  "AsyncAtlasIndexManager",
67
128
  "AutoIndexManager",
@@ -0,0 +1,112 @@
1
+ # Authorization Provider Architecture
2
+
3
+ ## Overview
4
+
5
+ The authorization system uses the **Adapter Pattern** with a strict abstract base class to ensure type safety, fail-closed security, and proper abstraction from third-party libraries.
6
+
7
+ ## Design Principles
8
+
9
+ 1. **Adapter Pattern**: We wrap third-party libraries (Casbin, OSO) without modifying their source code
10
+ 2. **Fail-Closed Security**: If authorization evaluation fails, access is denied (never granted)
11
+ 3. **Type Safety**: Clear contracts with proper type hints and abstract methods
12
+ 4. **Interface Segregation**: Application code only needs `check()`, not engine internals
13
+ 5. **Observability**: Structured logging for all authorization decisions and errors
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ BaseAuthorizationProvider (ABC)
19
+ ├── CasbinAdapter
20
+ │ └── Wraps casbin.AsyncEnforcer
21
+ └── OsoAdapter
22
+ └── Wraps oso_cloud.Client or oso.Oso
23
+ ```
24
+
25
+ ## Base Class: `BaseAuthorizationProvider`
26
+
27
+ Defines the contract that all authorization providers must implement:
28
+
29
+ - `check(subject, resource, action)` - Primary authorization decision method
30
+ - `add_policy(*params)` - Add policy rules
31
+ - `add_role_for_user(*params)` - Assign roles to users
32
+ - `save_policy()` - Persist policies to storage
33
+ - `has_policy(*params)` - Check if policy exists
34
+ - `has_role_for_user(*params)` - Check if user has role
35
+ - `clear_cache()` - Clear authorization cache
36
+
37
+ ### Fail-Closed Security
38
+
39
+ All `check()` implementations must:
40
+ 1. Catch all exceptions during evaluation
41
+ 2. Log errors with full context
42
+ 3. Return `False` (deny access) on any error
43
+ 4. Never raise exceptions from evaluation failures
44
+
45
+ ### Error Handling
46
+
47
+ - **Evaluation Errors**: Handled by `_handle_evaluation_error()` - denies access, logs critically
48
+ - **Operation Errors**: Handled by `_handle_operation_error()` - returns False, logs warning
49
+
50
+ ## CasbinAdapter
51
+
52
+ Wraps `casbin.AsyncEnforcer` and handles:
53
+ - Casbin's `(subject, object, action)` format
54
+ - Thread pool execution to prevent blocking
55
+ - Caching for performance
56
+ - MongoDB persistence via MotorAdapter
57
+
58
+ ### Format Mapping
59
+
60
+ - `check(subject, resource, action)` → `enforcer.enforce(subject, resource, action)`
61
+ - `add_policy(role, resource, action)` → `enforcer.add_policy(role, resource, action)`
62
+ - `add_role_for_user(user, role)` → `enforcer.add_role_for_user(user, role)`
63
+
64
+ ## OsoAdapter
65
+
66
+ Wraps OSO Cloud client and handles:
67
+ - OSO's `authorize(actor, action, resource)` format
68
+ - Type marshalling (strings → TypedObject)
69
+ - Thread pool execution
70
+ - Caching for performance
71
+
72
+ ### Format Mapping
73
+
74
+ - `check(subject, resource, action)` → `client.authorize(TypedObject("User", subject), action, TypedObject("Document", resource))`
75
+ - `add_policy(role, resource, action)` → `client.insert(["grants_permission", role, action, resource])`
76
+ - `add_role_for_user(user, role, [resource])` → `client.insert(["has_role", Value("User", user), role, Value("Document", resource)])`
77
+
78
+ ## Backward Compatibility
79
+
80
+ The `AuthorizationProvider` Protocol is maintained for backward compatibility. All adapters implement both:
81
+ - The Protocol (for structural typing)
82
+ - The BaseAuthorizationProvider ABC (for inheritance)
83
+
84
+ ## Usage
85
+
86
+ ```python
87
+ from mdb_engine.auth import BaseAuthorizationProvider, CasbinAdapter, OsoAdapter
88
+
89
+ # Type checking works with both Protocol and ABC
90
+ async def check_permission(
91
+ provider: BaseAuthorizationProvider, # or AuthorizationProvider
92
+ user: str,
93
+ resource: str,
94
+ action: str,
95
+ ) -> bool:
96
+ return await provider.check(user, resource, action)
97
+
98
+ # Runtime type checking
99
+ if provider.is_casbin():
100
+ # Casbin-specific operations
101
+ enforcer = provider._enforcer # Still accessible
102
+ elif provider.is_oso():
103
+ # OSO-specific operations
104
+ client = provider._oso # Still accessible
105
+ ```
106
+
107
+ ## Migration Notes
108
+
109
+ - Existing code using `AuthorizationProvider` Protocol continues to work
110
+ - Code checking `hasattr(provider, "_enforcer")` continues to work
111
+ - New code should use `BaseAuthorizationProvider` for better type safety
112
+ - Helper methods `is_casbin()` and `is_oso()` available for type checking
mdb_engine/auth/README.md CHANGED
@@ -926,18 +926,132 @@ CSRF protection is **auto-enabled for shared auth mode**. The middleware uses th
926
926
  4. SameSite=Lax cookies provide additional protection
927
927
 
928
928
  **Frontend Integration:**
929
+
930
+ Helper function for reading cookies:
931
+
932
+ ```javascript
933
+ // Reusable cookie helper
934
+ function getCookie(name) {
935
+ const value = `; ${document.cookie}`;
936
+ const parts = value.split(`; ${name}=`);
937
+ if (parts.length === 2) return parts.pop().split(';').shift();
938
+ return null;
939
+ }
940
+ ```
941
+
942
+ Include in all state-changing requests:
943
+
944
+ ```javascript
945
+ // POST request with CSRF token
946
+ async function createItem(data) {
947
+ const response = await fetch('/api/items', {
948
+ method: 'POST',
949
+ headers: {
950
+ 'Content-Type': 'application/json',
951
+ 'X-CSRF-Token': getCookie('csrf_token')
952
+ },
953
+ credentials: 'same-origin',
954
+ body: JSON.stringify(data)
955
+ });
956
+ return response.json();
957
+ }
958
+
959
+ // DELETE request with CSRF token
960
+ async function deleteItem(id) {
961
+ const response = await fetch(`/api/items/${id}`, {
962
+ method: 'DELETE',
963
+ headers: {
964
+ 'X-CSRF-Token': getCookie('csrf_token')
965
+ },
966
+ credentials: 'same-origin'
967
+ });
968
+ return response.json();
969
+ }
970
+ ```
971
+
972
+ **Logout Must Be POST:**
973
+
974
+ For security, logout endpoints should use POST method, not GET:
975
+
976
+ ```javascript
977
+ // Correct: POST with CSRF token
978
+ async function logout() {
979
+ const response = await fetch('/logout', {
980
+ method: 'POST',
981
+ headers: {
982
+ 'X-CSRF-Token': getCookie('csrf_token')
983
+ },
984
+ credentials: 'same-origin'
985
+ });
986
+
987
+ const result = await response.json();
988
+ if (result.success) {
989
+ window.location.href = result.redirect || '/login';
990
+ }
991
+ }
992
+ ```
993
+
994
+ Backend endpoint:
995
+
996
+ ```python
997
+ @app.post("/logout")
998
+ async def logout(request: Request):
999
+ """Logout must be POST with CSRF token."""
1000
+ response = JSONResponse({"success": True, "redirect": "/login"})
1001
+ response = await logout_user(request, response)
1002
+ return response
1003
+ ```
1004
+
1005
+ **Login/Register JSON Pattern:**
1006
+
1007
+ Return JSON responses for AJAX forms:
1008
+
1009
+ ```python
1010
+ @app.post("/login")
1011
+ async def login(request: Request, email: str = Form(...), password: str = Form(...)):
1012
+ """Login returning JSON for JavaScript frontend."""
1013
+ result = await authenticate_user(email, password)
1014
+
1015
+ if result["success"]:
1016
+ json_response = JSONResponse({"success": True, "redirect": "/dashboard"})
1017
+ # Copy auth cookies from result
1018
+ for key, value in result["response"].headers.items():
1019
+ if key.lower() == "set-cookie":
1020
+ json_response.headers.append(key, value)
1021
+ return json_response
1022
+
1023
+ return JSONResponse(
1024
+ {"success": False, "detail": result.get("error", "Login failed")},
1025
+ status_code=401
1026
+ )
1027
+ ```
1028
+
1029
+ **Error Handling:**
1030
+
1031
+ Handle CSRF validation failures (403 status):
1032
+
929
1033
  ```javascript
930
- // Read token from cookie
931
- const csrfToken = document.cookie
932
- .split('; ')
933
- .find(row => row.startsWith('csrf_token='))
934
- ?.split('=')[1];
935
-
936
- // Include in requests
937
- fetch('/api/submit', {
938
- method: 'POST',
939
- headers: {'X-CSRF-Token': csrfToken}
940
- });
1034
+ async function secureRequest(url, options = {}) {
1035
+ const response = await fetch(url, {
1036
+ ...options,
1037
+ headers: {
1038
+ ...options.headers,
1039
+ 'X-CSRF-Token': getCookie('csrf_token')
1040
+ },
1041
+ credentials: 'same-origin'
1042
+ });
1043
+
1044
+ if (response.status === 403) {
1045
+ const data = await response.json();
1046
+ if (data.detail?.includes('CSRF')) {
1047
+ // Token expired - refresh the page
1048
+ window.location.reload();
1049
+ return null;
1050
+ }
1051
+ }
1052
+
1053
+ return response;
1054
+ }
941
1055
  ```
942
1056
 
943
1057
  ### HSTS (HTTP Strict Transport Security)
@@ -9,6 +9,9 @@ This module is part of MDB_ENGINE - MongoDB Engine.
9
9
  # Audit logging
10
10
  from .audit import AuthAction, AuthAuditLog
11
11
 
12
+ # Base classes
13
+ from .base import AuthorizationError, BaseAuthorizationProvider
14
+
12
15
  # Casbin Factory
13
16
  from .casbin_factory import (
14
17
  create_casbin_enforcer,
@@ -123,7 +126,10 @@ from .utils import (
123
126
  )
124
127
 
125
128
  __all__ = [
126
- # Provider
129
+ # Base classes
130
+ "BaseAuthorizationProvider",
131
+ "AuthorizationError",
132
+ # Provider (Protocol for backward compatibility)
127
133
  "AuthorizationProvider",
128
134
  "CasbinAdapter",
129
135
  "OsoAdapter",
@@ -0,0 +1,252 @@
1
+ """
2
+ Authorization Engine Base Classes
3
+
4
+ Defines the abstract contract for authorization providers using the Adapter Pattern.
5
+ This ensures type safety, fail-closed security, and proper abstraction.
6
+
7
+ This module is part of MDB_ENGINE - MongoDB Engine.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import abc
13
+ import logging
14
+ from typing import Any, Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class AuthorizationError(Exception):
20
+ """
21
+ Base exception for authorization failures.
22
+
23
+ Ensures abstraction doesn't leak - application code doesn't need to know
24
+ if the failure came from Casbin, OSO, or any other engine.
25
+ """
26
+
27
+ pass
28
+
29
+
30
+ class BaseAuthorizationProvider(abc.ABC):
31
+ """
32
+ Abstract Base Class defining the contract for authorization providers.
33
+
34
+ Design Principles:
35
+ 1. Interface Segregation - Application only needs 'check', not engine internals
36
+ 2. Fail-Closed Security - Errors deny access, never grant it
37
+ 3. Adapter Pattern - Wraps third-party libraries without modifying them
38
+ 4. Type Safety - Clear contracts with proper type hints
39
+
40
+ This ABC ensures that all authorization providers:
41
+ - Have a consistent interface
42
+ - Fail securely (deny on error)
43
+ - Provide observability (structured logging)
44
+ - Handle edge cases gracefully
45
+ """
46
+
47
+ def __init__(self, engine_name: str):
48
+ """
49
+ Initialize the base provider.
50
+
51
+ Args:
52
+ engine_name: Human-readable name of the engine (e.g., "Casbin", "OSO Cloud")
53
+ """
54
+ self._engine_name = engine_name
55
+ self._initialized = False
56
+ logger.info(f"Initializing {engine_name} authorization provider")
57
+
58
+ @property
59
+ def engine_name(self) -> str:
60
+ """Get the name of the authorization engine."""
61
+ return self._engine_name
62
+
63
+ @property
64
+ def is_initialized(self) -> bool:
65
+ """Check if the provider has been properly initialized."""
66
+ return self._initialized
67
+
68
+ @abc.abstractmethod
69
+ async def check(
70
+ self,
71
+ subject: str,
72
+ resource: str,
73
+ action: str,
74
+ user_object: Optional[dict[str, Any]] = None,
75
+ ) -> bool:
76
+ """
77
+ Check if a subject is allowed to perform an action on a resource.
78
+
79
+ This is the primary authorization decision method. All implementations
80
+ must follow fail-closed security: if evaluation fails, deny access.
81
+
82
+ Args:
83
+ subject: Who is making the request (typically email or user ID)
84
+ resource: What resource they're accessing (e.g., "documents", "clicks")
85
+ action: What action they want to perform (e.g., "read", "write", "delete")
86
+ user_object: Optional user object with additional context
87
+
88
+ Returns:
89
+ True if authorized, False otherwise (including on error - fail-closed)
90
+
91
+ Raises:
92
+ AuthorizationError: Only for configuration/initialization errors,
93
+ not evaluation failures
94
+ """
95
+ pass
96
+
97
+ @abc.abstractmethod
98
+ async def add_policy(self, *params: Any) -> bool:
99
+ """
100
+ Add a policy rule to the authorization engine.
101
+
102
+ Args:
103
+ *params: Policy parameters (format depends on engine)
104
+
105
+ Returns:
106
+ True if policy was added successfully, False otherwise
107
+ """
108
+ pass
109
+
110
+ @abc.abstractmethod
111
+ async def add_role_for_user(self, *params: Any) -> bool:
112
+ """
113
+ Assign a role to a user.
114
+
115
+ Args:
116
+ *params: Role assignment parameters (format depends on engine)
117
+
118
+ Returns:
119
+ True if role was assigned successfully, False otherwise
120
+ """
121
+ pass
122
+
123
+ @abc.abstractmethod
124
+ async def save_policy(self) -> bool:
125
+ """
126
+ Persist policy changes to storage.
127
+
128
+ Returns:
129
+ True if saved successfully, False otherwise
130
+ """
131
+ pass
132
+
133
+ @abc.abstractmethod
134
+ async def has_policy(self, *params: Any) -> bool:
135
+ """
136
+ Check if a policy exists.
137
+
138
+ Args:
139
+ *params: Policy parameters to check
140
+
141
+ Returns:
142
+ True if policy exists, False otherwise
143
+ """
144
+ pass
145
+
146
+ @abc.abstractmethod
147
+ async def has_role_for_user(self, *params: Any) -> bool:
148
+ """
149
+ Check if a user has a specific role.
150
+
151
+ Args:
152
+ *params: User and role parameters
153
+
154
+ Returns:
155
+ True if user has the role, False otherwise
156
+ """
157
+ pass
158
+
159
+ @abc.abstractmethod
160
+ async def clear_cache(self) -> None:
161
+ """
162
+ Clear the authorization cache.
163
+
164
+ Should be called when policies or roles are modified to ensure
165
+ fresh authorization decisions.
166
+ """
167
+ pass
168
+
169
+ def _mark_initialized(self) -> None:
170
+ """Mark the provider as initialized (internal use only)."""
171
+ self._initialized = True
172
+ logger.info(f"✅ {self._engine_name} authorization provider initialized successfully")
173
+
174
+ def is_casbin(self) -> bool:
175
+ """
176
+ Check if this provider is a Casbin adapter.
177
+
178
+ Returns:
179
+ True if this is a CasbinAdapter, False otherwise
180
+ """
181
+ return hasattr(self, "_enforcer")
182
+
183
+ def is_oso(self) -> bool:
184
+ """
185
+ Check if this provider is an OSO adapter.
186
+
187
+ Returns:
188
+ True if this is an OsoAdapter, False otherwise
189
+ """
190
+ return hasattr(self, "_oso")
191
+
192
+ def _handle_evaluation_error(
193
+ self,
194
+ subject: str,
195
+ resource: str,
196
+ action: str,
197
+ error: Exception,
198
+ context: Optional[str] = None,
199
+ ) -> bool:
200
+ """
201
+ Handle authorization evaluation errors with fail-closed security.
202
+
203
+ Design Principle: Fail-Closed Security
204
+ - If the authorization engine crashes or errors, we MUST deny access
205
+ - Logging the error is critical for observability
206
+ - Never raise exceptions from evaluation errors (only from config errors)
207
+
208
+ Args:
209
+ subject: Subject that was being checked
210
+ resource: Resource that was being checked
211
+ action: Action that was being checked
212
+ error: The exception that occurred
213
+ context: Optional context string for logging
214
+
215
+ Returns:
216
+ False (deny access - fail-closed)
217
+ """
218
+ context_str = f" ({context})" if context else ""
219
+ logger.critical(
220
+ f"{self._engine_name} authorization evaluation failed{context_str}: "
221
+ f"subject={subject}, resource={resource}, action={action}, "
222
+ f"error={type(error).__name__}: {error}",
223
+ exc_info=True,
224
+ )
225
+ return False
226
+
227
+ def _handle_operation_error(
228
+ self,
229
+ operation: str,
230
+ error: Exception,
231
+ *params: Any,
232
+ ) -> bool:
233
+ """
234
+ Handle policy/role operation errors.
235
+
236
+ These are non-critical operations (adding policies, roles, etc.)
237
+ so we log warnings but don't fail-closed (return False).
238
+
239
+ Args:
240
+ operation: Name of the operation (e.g., "add_policy")
241
+ error: The exception that occurred
242
+ *params: Parameters that were passed to the operation
243
+
244
+ Returns:
245
+ False (operation failed)
246
+ """
247
+ logger.warning(
248
+ f"{self._engine_name} {operation} failed: "
249
+ f"params={params}, error={type(error).__name__}: {error}",
250
+ exc_info=True,
251
+ )
252
+ return False