mdb-engine 0.1.7__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mdb_engine/__init__.py +71 -10
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +125 -11
- mdb_engine/auth/__init__.py +7 -1
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +258 -59
- mdb_engine/auth/dependencies.py +10 -5
- mdb_engine/auth/integration.py +23 -7
- mdb_engine/auth/oso_factory.py +2 -2
- mdb_engine/auth/provider.py +263 -143
- mdb_engine/core/engine.py +307 -6
- mdb_engine/core/manifest.py +35 -15
- mdb_engine/database/README.md +28 -1
- mdb_engine/dependencies.py +426 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +248 -0
- mdb_engine/di/providers.py +205 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +22 -23
- mdb_engine/embeddings/dependencies.py +37 -152
- mdb_engine/repositories/__init__.py +34 -0
- mdb_engine/repositories/base.py +325 -0
- mdb_engine/repositories/mongo.py +233 -0
- mdb_engine/repositories/unit_of_work.py +166 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.1.dist-info}/METADATA +42 -14
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.1.dist-info}/RECORD +31 -20
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.1.dist-info}/WHEEL +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.1.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.1.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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
|
-
#
|
|
18
|
-
|
|
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,
|
|
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
|
-
|
|
78
|
+
# Repository pattern
|
|
79
|
+
from .repositories import Entity, MongoRepository, Repository, UnitOfWork
|
|
80
|
+
|
|
81
|
+
__version__ = "0.2.1" # Major version bump for new DI system
|
|
47
82
|
|
|
48
83
|
__all__ = [
|
|
49
|
-
# Core
|
|
84
|
+
# Core Engine
|
|
50
85
|
"MongoDBEngine",
|
|
51
86
|
"ManifestValidator",
|
|
52
87
|
"ManifestParser",
|
|
53
|
-
# Ray Integration (optional
|
|
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
|
-
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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)
|
mdb_engine/auth/__init__.py
CHANGED
|
@@ -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
|
-
#
|
|
129
|
+
# Base classes
|
|
130
|
+
"BaseAuthorizationProvider",
|
|
131
|
+
"AuthorizationError",
|
|
132
|
+
# Provider (Protocol for backward compatibility)
|
|
127
133
|
"AuthorizationProvider",
|
|
128
134
|
"CasbinAdapter",
|
|
129
135
|
"OsoAdapter",
|
mdb_engine/auth/base.py
ADDED
|
@@ -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
|