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/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
|
|
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: dict[str, Any] | None = 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: str | None = 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
|
|
@@ -11,19 +11,19 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import TYPE_CHECKING, Any
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
15
|
|
|
16
16
|
from .casbin_models import DEFAULT_RBAC_MODEL, SIMPLE_ACL_MODEL
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
|
-
import casbin
|
|
19
|
+
import casbin # type: ignore
|
|
20
20
|
|
|
21
21
|
from .provider import CasbinAdapter
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def get_casbin_model(model_type: str = "rbac") -> str:
|
|
26
|
+
async def get_casbin_model(model_type: str = "rbac") -> str:
|
|
27
27
|
"""
|
|
28
28
|
Get Casbin model string by type or path.
|
|
29
29
|
|
|
@@ -39,29 +39,44 @@ def get_casbin_model(model_type: str = "rbac") -> str:
|
|
|
39
39
|
return SIMPLE_ACL_MODEL
|
|
40
40
|
else:
|
|
41
41
|
# Assume it's a file path
|
|
42
|
+
# Try async file reading first, fallback to sync if not available
|
|
42
43
|
model_path = Path(model_type)
|
|
43
44
|
if model_path.exists():
|
|
44
|
-
|
|
45
|
+
try:
|
|
46
|
+
# Try async file reading (non-blocking)
|
|
47
|
+
import aiofiles
|
|
48
|
+
|
|
49
|
+
async with aiofiles.open(model_path) as f:
|
|
50
|
+
content = await f.read()
|
|
51
|
+
logger.debug(f"Read model file asynchronously: {model_path}")
|
|
52
|
+
return content
|
|
53
|
+
except ImportError:
|
|
54
|
+
# Fallback to sync read if aiofiles not available
|
|
55
|
+
# This is acceptable during startup initialization
|
|
56
|
+
logger.debug(
|
|
57
|
+
"aiofiles not available, using sync file read (acceptable during startup)"
|
|
58
|
+
)
|
|
59
|
+
return model_path.read_text()
|
|
45
60
|
else:
|
|
46
|
-
logger.warning(
|
|
47
|
-
f"Casbin model file not found: {model_type}, using default RBAC model"
|
|
48
|
-
)
|
|
61
|
+
logger.warning(f"Casbin model file not found: {model_type}, using default RBAC model")
|
|
49
62
|
return DEFAULT_RBAC_MODEL
|
|
50
63
|
|
|
51
64
|
|
|
52
65
|
async def create_casbin_enforcer(
|
|
53
|
-
|
|
66
|
+
mongo_uri: str,
|
|
67
|
+
db_name: str,
|
|
54
68
|
model: str = "rbac",
|
|
55
69
|
policies_collection: str = "casbin_policies",
|
|
56
|
-
default_roles:
|
|
70
|
+
default_roles: list | None = None,
|
|
57
71
|
) -> casbin.AsyncEnforcer:
|
|
58
72
|
"""
|
|
59
73
|
Create a Casbin AsyncEnforcer with MongoDB adapter.
|
|
60
74
|
|
|
61
75
|
Args:
|
|
62
|
-
|
|
76
|
+
mongo_uri: MongoDB connection URI string
|
|
77
|
+
db_name: MongoDB database name
|
|
63
78
|
model: Casbin model type ("rbac", "acl") or path to model file
|
|
64
|
-
policies_collection: MongoDB collection name for policies
|
|
79
|
+
policies_collection: MongoDB collection name for policies (will be app-scoped)
|
|
65
80
|
default_roles: List of default roles to create (optional)
|
|
66
81
|
|
|
67
82
|
Returns:
|
|
@@ -71,30 +86,94 @@ async def create_casbin_enforcer(
|
|
|
71
86
|
ImportError: If casbin or casbin-motor-adapter is not installed
|
|
72
87
|
"""
|
|
73
88
|
try:
|
|
74
|
-
import casbin
|
|
75
|
-
from casbin_motor_adapter import
|
|
89
|
+
import casbin # type: ignore
|
|
90
|
+
from casbin_motor_adapter import Adapter # type: ignore
|
|
76
91
|
except ImportError as e:
|
|
77
92
|
raise ImportError(
|
|
78
93
|
"Casbin dependencies not installed. Install with: pip install mdb-engine[casbin]"
|
|
79
94
|
) from e
|
|
80
95
|
|
|
81
|
-
# Get model string
|
|
82
|
-
model_str = get_casbin_model(model)
|
|
96
|
+
# Get model string (async)
|
|
97
|
+
model_str = await get_casbin_model(model)
|
|
83
98
|
|
|
84
99
|
# Create MongoDB adapter
|
|
85
|
-
|
|
100
|
+
# Try to pass policies_collection if supported by the adapter
|
|
101
|
+
logger.debug(
|
|
102
|
+
f"Creating Casbin MotorAdapter with URI: {mongo_uri[:50]}..., "
|
|
103
|
+
f"db_name: {db_name}, collection: {policies_collection}"
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
# Try passing collection name as third parameter
|
|
107
|
+
try:
|
|
108
|
+
adapter = Adapter(mongo_uri, db_name, policies_collection)
|
|
109
|
+
logger.debug(
|
|
110
|
+
f"Casbin MotorAdapter created successfully with custom "
|
|
111
|
+
f"collection '{policies_collection}'"
|
|
112
|
+
)
|
|
113
|
+
except TypeError:
|
|
114
|
+
# Fallback: adapter doesn't support collection parameter, use default
|
|
115
|
+
logger.warning(
|
|
116
|
+
"Adapter doesn't support custom collection name, " "using default collection"
|
|
117
|
+
)
|
|
118
|
+
adapter = Adapter(mongo_uri, db_name)
|
|
119
|
+
logger.debug("Casbin MotorAdapter created successfully (using default collection)")
|
|
120
|
+
except (RuntimeError, ValueError, AttributeError, TypeError, ConnectionError):
|
|
121
|
+
logger.exception("Failed to create Casbin MotorAdapter")
|
|
122
|
+
raise
|
|
86
123
|
|
|
87
124
|
# Create enforcer with model and adapter
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
125
|
+
# AsyncEnforcer can accept model string or Model object
|
|
126
|
+
logger.debug("Creating Casbin AsyncEnforcer with model and adapter...")
|
|
127
|
+
try:
|
|
128
|
+
logger.debug(f"Model string length: {len(model_str)} chars")
|
|
129
|
+
logger.debug(f"Adapter type: {type(adapter)}")
|
|
130
|
+
|
|
131
|
+
# Create Model object from string
|
|
132
|
+
model = casbin.Model()
|
|
133
|
+
model.load_model_from_text(model_str)
|
|
134
|
+
logger.debug(f"Model object created successfully, type: {type(model)}")
|
|
135
|
+
|
|
136
|
+
# Create enforcer with Model object and adapter
|
|
137
|
+
# AsyncEnforcer auto-loads by default (auto_load=True), so we don't need manual load
|
|
138
|
+
logger.debug("Calling casbin.AsyncEnforcer(model, adapter)...")
|
|
139
|
+
enforcer = casbin.AsyncEnforcer(model, adapter)
|
|
140
|
+
logger.debug("Enforcer created successfully")
|
|
141
|
+
|
|
142
|
+
# Check if policies were auto-loaded
|
|
143
|
+
# If auto_load is enabled (default), policies are already loaded
|
|
144
|
+
# Only manually load if auto_load was disabled
|
|
145
|
+
if not getattr(enforcer, "auto_load", True):
|
|
146
|
+
logger.debug("Auto-load disabled, manually loading policies...")
|
|
147
|
+
await enforcer.load_policy()
|
|
148
|
+
logger.debug("Policies loaded successfully")
|
|
149
|
+
else:
|
|
150
|
+
logger.debug("Policies auto-loaded by AsyncEnforcer constructor")
|
|
151
|
+
except (RuntimeError, ValueError, AttributeError, TypeError, OSError):
|
|
152
|
+
logger.exception("Failed to create or configure Casbin enforcer")
|
|
153
|
+
# Try alternative: use temp file approach
|
|
154
|
+
logger.info("Attempting alternative: using temporary model file...")
|
|
155
|
+
try:
|
|
156
|
+
import os
|
|
157
|
+
import tempfile
|
|
158
|
+
|
|
159
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".conf", delete=False) as f:
|
|
160
|
+
f.write(model_str)
|
|
161
|
+
temp_model_path = f.name
|
|
162
|
+
try:
|
|
163
|
+
enforcer = casbin.AsyncEnforcer(temp_model_path, adapter)
|
|
164
|
+
logger.info("✅ Enforcer created successfully using temp model file")
|
|
165
|
+
# Clean up temp file
|
|
166
|
+
os.unlink(temp_model_path)
|
|
167
|
+
except (RuntimeError, ValueError, AttributeError, TypeError, OSError) as e2:
|
|
168
|
+
if os.path.exists(temp_model_path):
|
|
169
|
+
os.unlink(temp_model_path)
|
|
170
|
+
logger.exception("Alternative approach also failed")
|
|
171
|
+
raise RuntimeError("Failed to create Casbin enforcer") from e2
|
|
172
|
+
except (OSError, RuntimeError) as e2:
|
|
173
|
+
logger.exception("Failed to try alternative approach")
|
|
174
|
+
raise RuntimeError("Failed to create Casbin enforcer") from e2
|
|
175
|
+
|
|
176
|
+
# Note: Removed default_roles creation - roles exist implicitly when assigned to users
|
|
98
177
|
|
|
99
178
|
logger.info(
|
|
100
179
|
f"Casbin enforcer created with model '{model}' and "
|
|
@@ -104,40 +183,20 @@ async def create_casbin_enforcer(
|
|
|
104
183
|
return enforcer
|
|
105
184
|
|
|
106
185
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
Create default roles in Casbin (as grouping rules).
|
|
110
|
-
|
|
111
|
-
Args:
|
|
112
|
-
enforcer: Casbin AsyncEnforcer instance
|
|
113
|
-
roles: List of role names to create
|
|
114
|
-
"""
|
|
115
|
-
for role in roles:
|
|
116
|
-
# Create a grouping rule: role -> role (self-reference for role existence)
|
|
117
|
-
# This ensures the role exists in the system
|
|
118
|
-
# Actual user-role assignments will be added when users are created
|
|
119
|
-
try:
|
|
120
|
-
# Check if role already exists
|
|
121
|
-
existing = await enforcer.get_roles_for_user(role)
|
|
122
|
-
if not existing:
|
|
123
|
-
# Add role as a self-grouping rule to ensure it exists
|
|
124
|
-
# This is a common pattern to "register" roles
|
|
125
|
-
await enforcer.add_grouping_policy(role, role)
|
|
126
|
-
logger.debug(f"Created default Casbin role: {role}")
|
|
127
|
-
except (AttributeError, TypeError, ValueError, RuntimeError) as e:
|
|
128
|
-
logger.warning(f"Error creating default role '{role}': {e}")
|
|
186
|
+
# Removed _create_default_roles function - roles exist implicitly when assigned to users
|
|
187
|
+
# No need to "create" roles beforehand with self-referencing policies
|
|
129
188
|
|
|
130
189
|
|
|
131
190
|
async def initialize_casbin_from_manifest(
|
|
132
|
-
engine, app_slug: str, auth_config:
|
|
133
|
-
) ->
|
|
191
|
+
engine, app_slug: str, auth_config: dict[str, Any]
|
|
192
|
+
) -> CasbinAdapter | None:
|
|
134
193
|
"""
|
|
135
194
|
Initialize Casbin provider from manifest configuration.
|
|
136
195
|
|
|
137
196
|
Args:
|
|
138
197
|
engine: MongoDBEngine instance
|
|
139
198
|
app_slug: App slug identifier
|
|
140
|
-
auth_config:
|
|
199
|
+
auth_config: Full manifest config dict (contains auth.policy or auth_policy)
|
|
141
200
|
|
|
142
201
|
Returns:
|
|
143
202
|
CasbinAdapter instance if successfully created, None otherwise
|
|
@@ -145,42 +204,176 @@ async def initialize_casbin_from_manifest(
|
|
|
145
204
|
try:
|
|
146
205
|
from .provider import CasbinAdapter
|
|
147
206
|
|
|
148
|
-
|
|
207
|
+
# Support both old (auth_policy) and new (auth.policy) structures
|
|
208
|
+
auth = auth_config.get("auth", {})
|
|
209
|
+
auth_policy = auth.get("policy", {}) or auth_config.get("auth_policy", {})
|
|
149
210
|
provider = auth_policy.get("provider", "casbin")
|
|
150
211
|
|
|
151
212
|
# Only proceed if provider is casbin
|
|
152
213
|
if provider != "casbin":
|
|
214
|
+
logger.debug(f"Provider is '{provider}', not 'casbin' - skipping Casbin initialization")
|
|
153
215
|
return None
|
|
154
216
|
|
|
217
|
+
logger.info(f"Initializing Casbin provider for app '{app_slug}'...")
|
|
218
|
+
|
|
155
219
|
# Get authorization config
|
|
156
220
|
authorization = auth_policy.get("authorization", {})
|
|
157
221
|
model = authorization.get("model", "rbac")
|
|
158
|
-
policies_collection = authorization.get(
|
|
159
|
-
"policies_collection", "casbin_policies"
|
|
160
|
-
)
|
|
222
|
+
policies_collection = authorization.get("policies_collection", "casbin_policies")
|
|
161
223
|
default_roles = authorization.get("default_roles", [])
|
|
224
|
+
initial_policies = authorization.get("initial_policies", [])
|
|
225
|
+
initial_roles = authorization.get("initial_roles", [])
|
|
162
226
|
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
enforcer = await create_casbin_enforcer(
|
|
168
|
-
db=db,
|
|
169
|
-
model=model,
|
|
170
|
-
policies_collection=policies_collection,
|
|
171
|
-
default_roles=default_roles,
|
|
227
|
+
# Create enforcer with MongoDB connection info from engine
|
|
228
|
+
logger.debug(
|
|
229
|
+
f"Creating Casbin enforcer with URI: {engine.mongo_uri[:50]}..., "
|
|
230
|
+
f"db: {engine.db_name}"
|
|
172
231
|
)
|
|
232
|
+
try:
|
|
233
|
+
enforcer = await create_casbin_enforcer(
|
|
234
|
+
mongo_uri=engine.mongo_uri,
|
|
235
|
+
db_name=engine.db_name,
|
|
236
|
+
model=model,
|
|
237
|
+
policies_collection=policies_collection,
|
|
238
|
+
default_roles=default_roles,
|
|
239
|
+
)
|
|
240
|
+
logger.debug("Casbin enforcer created successfully")
|
|
241
|
+
except (
|
|
242
|
+
RuntimeError,
|
|
243
|
+
ValueError,
|
|
244
|
+
AttributeError,
|
|
245
|
+
TypeError,
|
|
246
|
+
ConnectionError,
|
|
247
|
+
ImportError,
|
|
248
|
+
):
|
|
249
|
+
logger.exception(f"Failed to create Casbin enforcer for '{app_slug}'")
|
|
250
|
+
return None
|
|
173
251
|
|
|
174
252
|
# Create adapter
|
|
175
|
-
|
|
253
|
+
try:
|
|
254
|
+
adapter = CasbinAdapter(enforcer)
|
|
255
|
+
logger.info("✅ CasbinAdapter created successfully")
|
|
256
|
+
except (RuntimeError, ValueError, AttributeError, TypeError):
|
|
257
|
+
logger.exception(f"Failed to create CasbinAdapter for '{app_slug}'")
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
# Set up initial policies if configured
|
|
261
|
+
if initial_policies:
|
|
262
|
+
logger.info(f"Setting up {len(initial_policies)} initial policies...")
|
|
263
|
+
for policy in initial_policies:
|
|
264
|
+
if isinstance(policy, list | tuple) and len(policy) >= 3:
|
|
265
|
+
role, resource, action = policy[0], policy[1], policy[2]
|
|
266
|
+
try:
|
|
267
|
+
# Check if policy already exists
|
|
268
|
+
exists = await adapter.has_policy(role, resource, action)
|
|
269
|
+
if exists:
|
|
270
|
+
logger.debug(f" Policy already exists: {role} -> {resource}:{action}")
|
|
271
|
+
else:
|
|
272
|
+
added = await adapter.add_policy(role, resource, action)
|
|
273
|
+
if added:
|
|
274
|
+
logger.debug(f" Added policy: {role} -> {resource}:{action}")
|
|
275
|
+
else:
|
|
276
|
+
logger.warning(
|
|
277
|
+
f" Failed to add policy: {role} -> " f"{resource}:{action}"
|
|
278
|
+
)
|
|
279
|
+
except (ValueError, TypeError, RuntimeError, AttributeError) as e:
|
|
280
|
+
logger.warning(f" Failed to add policy {policy}: {e}", exc_info=True)
|
|
281
|
+
|
|
282
|
+
# Set up initial role assignments if configured
|
|
283
|
+
# Disable auto_save during bulk operations for better performance
|
|
284
|
+
if initial_roles:
|
|
285
|
+
logger.info(f"Setting up {len(initial_roles)} initial role assignments...")
|
|
286
|
+
# Temporarily disable auto_save to avoid writing on every iteration
|
|
287
|
+
original_auto_save = getattr(enforcer, "auto_save", True)
|
|
288
|
+
try:
|
|
289
|
+
enforcer.auto_save = False
|
|
290
|
+
logger.debug("Disabled auto_save for bulk role assignment")
|
|
291
|
+
except AttributeError:
|
|
292
|
+
# Some enforcer implementations don't support auto_save attribute
|
|
293
|
+
logger.debug("auto_save attribute not available, proceeding with default behavior")
|
|
294
|
+
|
|
295
|
+
for role_assignment in initial_roles:
|
|
296
|
+
if isinstance(role_assignment, dict):
|
|
297
|
+
user = role_assignment.get("user")
|
|
298
|
+
role = role_assignment.get("role")
|
|
299
|
+
if user and role:
|
|
300
|
+
try:
|
|
301
|
+
# Check if role assignment already exists
|
|
302
|
+
exists = await adapter.has_role_for_user(user, role)
|
|
303
|
+
logger.debug(
|
|
304
|
+
f" Checking role assignment: {user} -> {role}, " f"exists={exists}"
|
|
305
|
+
)
|
|
306
|
+
if exists:
|
|
307
|
+
logger.info(f" ✓ Role assignment already exists: {user} -> {role}")
|
|
308
|
+
else:
|
|
309
|
+
logger.info(f" Adding role assignment: {user} -> {role}")
|
|
310
|
+
# Use enforcer directly with add_grouping_policy
|
|
311
|
+
added = await enforcer.add_grouping_policy(user, role)
|
|
312
|
+
if added:
|
|
313
|
+
logger.info(
|
|
314
|
+
f" ✓ Successfully assigned role '{role}' "
|
|
315
|
+
f"to user '{user}'"
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
logger.warning(
|
|
319
|
+
f" ⚠ Failed to assign role '{role}' to "
|
|
320
|
+
f"user '{user}' - add_grouping_policy returned False"
|
|
321
|
+
)
|
|
322
|
+
except (ValueError, TypeError, RuntimeError, AttributeError):
|
|
323
|
+
logger.exception(f" ✗ Exception assigning role {role_assignment}")
|
|
324
|
+
|
|
325
|
+
# Restore auto_save setting
|
|
326
|
+
if hasattr(enforcer, "auto_save"):
|
|
327
|
+
enforcer.auto_save = original_auto_save
|
|
328
|
+
logger.debug("Restored auto_save setting")
|
|
329
|
+
|
|
330
|
+
# Verify that policies and roles were set up correctly
|
|
331
|
+
if initial_policies:
|
|
332
|
+
verified = 0
|
|
333
|
+
for policy in initial_policies:
|
|
334
|
+
if isinstance(policy, list | tuple) and len(policy) >= 3:
|
|
335
|
+
role, resource, action = policy[0], policy[1], policy[2]
|
|
336
|
+
if await adapter.has_policy(role, resource, action):
|
|
337
|
+
verified += 1
|
|
338
|
+
logger.info(f"Verified {verified}/{len(initial_policies)} policies exist in memory")
|
|
339
|
+
|
|
340
|
+
if initial_roles:
|
|
341
|
+
verified = 0
|
|
342
|
+
for role_assignment in initial_roles:
|
|
343
|
+
if isinstance(role_assignment, dict):
|
|
344
|
+
user = role_assignment.get("user")
|
|
345
|
+
role = role_assignment.get("role")
|
|
346
|
+
if user and role and await adapter.has_role_for_user(user, role):
|
|
347
|
+
verified += 1
|
|
348
|
+
logger.info(
|
|
349
|
+
f"Verified {verified}/{len(initial_roles)} role assignments " f"exist in memory"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Save policies to persist them to database
|
|
353
|
+
# Only save if auto_save was disabled (to avoid double-saving)
|
|
354
|
+
if not getattr(enforcer, "auto_save", True):
|
|
355
|
+
saved = await adapter.save_policy()
|
|
356
|
+
if saved:
|
|
357
|
+
logger.debug("Policies saved to database successfully")
|
|
358
|
+
else:
|
|
359
|
+
logger.warning(
|
|
360
|
+
"Failed to save policies to database - they may not " "persist across restarts"
|
|
361
|
+
)
|
|
362
|
+
else:
|
|
363
|
+
logger.debug(
|
|
364
|
+
"Skipping manual save_policy() - auto_save is enabled, "
|
|
365
|
+
"policies already persisted"
|
|
366
|
+
)
|
|
176
367
|
|
|
177
|
-
logger.info(f"Casbin provider initialized for app '{app_slug}'")
|
|
368
|
+
logger.info(f"✅ Casbin provider initialized for app '{app_slug}'")
|
|
369
|
+
logger.info(f"✅ CasbinAdapter ready for use - type: {type(adapter).__name__}")
|
|
178
370
|
|
|
179
371
|
return adapter
|
|
180
372
|
|
|
181
373
|
except ImportError as e:
|
|
374
|
+
# ImportError is expected if Casbin is not installed - use warning, not error
|
|
182
375
|
logger.warning(
|
|
183
|
-
f"Casbin not available for app '{app_slug}': {e}. "
|
|
376
|
+
f"❌ Casbin not available for app '{app_slug}': {e}. "
|
|
184
377
|
"Install with: pip install mdb-engine[casbin]"
|
|
185
378
|
)
|
|
186
379
|
return None
|
|
@@ -192,8 +385,10 @@ async def initialize_casbin_from_manifest(
|
|
|
192
385
|
RuntimeError,
|
|
193
386
|
KeyError,
|
|
194
387
|
) as e:
|
|
195
|
-
logger.
|
|
196
|
-
|
|
197
|
-
|
|
388
|
+
logger.exception(f"❌ Error initializing Casbin provider for app '{app_slug}': {e}")
|
|
389
|
+
# Informational message, not exception logging
|
|
390
|
+
logger.error( # noqa: TRY400
|
|
391
|
+
f"❌ Casbin provider initialization FAILED for '{app_slug}' - "
|
|
392
|
+
"check logs above for detailed error information"
|
|
198
393
|
)
|
|
199
394
|
return None
|