mdb-engine 0.1.6__py3-none-any.whl → 0.1.7__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 +38 -6
- mdb_engine/auth/README.md +534 -11
- mdb_engine/auth/__init__.py +129 -28
- mdb_engine/auth/audit.py +592 -0
- mdb_engine/auth/casbin_factory.py +10 -14
- mdb_engine/auth/config_helpers.py +7 -6
- mdb_engine/auth/cookie_utils.py +3 -7
- mdb_engine/auth/csrf.py +373 -0
- mdb_engine/auth/decorators.py +3 -10
- mdb_engine/auth/dependencies.py +37 -45
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +30 -73
- mdb_engine/auth/jwt.py +2 -6
- mdb_engine/auth/middleware.py +77 -34
- mdb_engine/auth/oso_factory.py +16 -36
- mdb_engine/auth/provider.py +17 -38
- mdb_engine/auth/rate_limiter.py +504 -0
- mdb_engine/auth/restrictions.py +8 -24
- mdb_engine/auth/session_manager.py +14 -29
- mdb_engine/auth/shared_middleware.py +600 -0
- mdb_engine/auth/shared_users.py +759 -0
- mdb_engine/auth/token_store.py +14 -28
- mdb_engine/auth/users.py +54 -113
- mdb_engine/auth/utils.py +213 -15
- mdb_engine/cli/commands/generate.py +545 -9
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +3 -3
- mdb_engine/config.py +7 -21
- 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 +22 -41
- mdb_engine/core/app_secrets.py +290 -0
- mdb_engine/core/connection.py +18 -9
- mdb_engine/core/encryption.py +223 -0
- mdb_engine/core/engine.py +758 -95
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +424 -135
- mdb_engine/core/ray_integration.py +435 -0
- mdb_engine/core/seeding.py +10 -18
- mdb_engine/core/service_initialization.py +12 -23
- mdb_engine/core/types.py +2 -5
- mdb_engine/database/README.md +112 -16
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +25 -37
- mdb_engine/database/connection.py +11 -18
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +713 -196
- mdb_engine/embeddings/__init__.py +17 -9
- mdb_engine/embeddings/dependencies.py +1 -3
- mdb_engine/embeddings/service.py +11 -25
- mdb_engine/exceptions.py +92 -0
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +1 -1
- mdb_engine/indexes/manager.py +50 -114
- mdb_engine/memory/README.md +2 -2
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +30 -87
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +8 -9
- mdb_engine/observability/metrics.py +32 -12
- mdb_engine/routing/README.md +1 -1
- mdb_engine/routing/__init__.py +1 -3
- mdb_engine/routing/websockets.py +25 -60
- mdb_engine-0.1.7.dist-info/METADATA +285 -0
- mdb_engine-0.1.7.dist-info/RECORD +85 -0
- 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.1.7.dist-info}/WHEEL +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.1.7.dist-info}/top_level.txt +0 -0
mdb_engine/auth/audit.py
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication Audit Logging
|
|
3
|
+
|
|
4
|
+
Structured audit logging for authentication events. Provides forensics,
|
|
5
|
+
compliance support, and security monitoring capabilities.
|
|
6
|
+
|
|
7
|
+
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Structured event logging to MongoDB
|
|
11
|
+
- TTL-based automatic cleanup
|
|
12
|
+
- Query helpers for security analysis
|
|
13
|
+
- Integration with shared auth middleware
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
# Initialize audit logger
|
|
17
|
+
audit = AuthAuditLog(mongo_db)
|
|
18
|
+
|
|
19
|
+
# Log authentication events
|
|
20
|
+
await audit.log_event(
|
|
21
|
+
action=AuthAction.LOGIN_SUCCESS,
|
|
22
|
+
user_email="user@example.com",
|
|
23
|
+
success=True,
|
|
24
|
+
ip_address="192.168.1.1",
|
|
25
|
+
user_agent="Mozilla/5.0...",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Query for security analysis
|
|
29
|
+
failed_logins = await audit.get_failed_logins(
|
|
30
|
+
email="user@example.com",
|
|
31
|
+
hours=24,
|
|
32
|
+
)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
from datetime import datetime, timedelta
|
|
37
|
+
from enum import Enum
|
|
38
|
+
from typing import Any, Dict, List, Optional
|
|
39
|
+
|
|
40
|
+
from motor.motor_asyncio import AsyncIOMotorDatabase
|
|
41
|
+
from pymongo.errors import OperationFailure
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# Collection name for audit logs
|
|
46
|
+
AUDIT_COLLECTION = "_mdb_engine_auth_audit"
|
|
47
|
+
|
|
48
|
+
# Default retention period (90 days)
|
|
49
|
+
DEFAULT_RETENTION_DAYS = 90
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AuthAction(str, Enum):
|
|
53
|
+
"""Authentication action types for audit logging."""
|
|
54
|
+
|
|
55
|
+
# Authentication events
|
|
56
|
+
LOGIN_SUCCESS = "login_success"
|
|
57
|
+
LOGIN_FAILED = "login_failed"
|
|
58
|
+
LOGOUT = "logout"
|
|
59
|
+
REGISTER = "register"
|
|
60
|
+
|
|
61
|
+
# Token events
|
|
62
|
+
TOKEN_REFRESHED = "token_refreshed"
|
|
63
|
+
TOKEN_REVOKED = "token_revoked"
|
|
64
|
+
TOKEN_EXPIRED = "token_expired"
|
|
65
|
+
|
|
66
|
+
# Account events
|
|
67
|
+
PASSWORD_CHANGED = "password_changed"
|
|
68
|
+
PASSWORD_RESET_REQUESTED = "password_reset_requested"
|
|
69
|
+
PASSWORD_RESET_COMPLETED = "password_reset_completed"
|
|
70
|
+
|
|
71
|
+
# Role/permission events
|
|
72
|
+
ROLE_GRANTED = "role_granted"
|
|
73
|
+
ROLE_REVOKED = "role_revoked"
|
|
74
|
+
|
|
75
|
+
# Security events
|
|
76
|
+
ACCOUNT_LOCKED = "account_locked"
|
|
77
|
+
ACCOUNT_UNLOCKED = "account_unlocked"
|
|
78
|
+
RATE_LIMIT_EXCEEDED = "rate_limit_exceeded"
|
|
79
|
+
SUSPICIOUS_ACTIVITY = "suspicious_activity"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AuthAuditLog:
|
|
83
|
+
"""
|
|
84
|
+
Manages structured audit logging for authentication events.
|
|
85
|
+
|
|
86
|
+
Logs are stored in MongoDB with TTL index for automatic cleanup.
|
|
87
|
+
Provides query helpers for security analysis and compliance.
|
|
88
|
+
|
|
89
|
+
Schema for audit documents:
|
|
90
|
+
{
|
|
91
|
+
"_id": ObjectId,
|
|
92
|
+
"timestamp": datetime,
|
|
93
|
+
"action": str (AuthAction value),
|
|
94
|
+
"success": bool,
|
|
95
|
+
"user_email": str | None,
|
|
96
|
+
"user_id": str | None,
|
|
97
|
+
"app_slug": str | None,
|
|
98
|
+
"ip_address": str | None,
|
|
99
|
+
"user_agent": str | None,
|
|
100
|
+
"details": dict | None,
|
|
101
|
+
"expires_at": datetime (for TTL)
|
|
102
|
+
}
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
mongo_db: AsyncIOMotorDatabase,
|
|
108
|
+
retention_days: int = DEFAULT_RETENTION_DAYS,
|
|
109
|
+
):
|
|
110
|
+
"""
|
|
111
|
+
Initialize the audit logger.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
mongo_db: MongoDB database instance
|
|
115
|
+
retention_days: How long to keep audit logs (default: 90 days)
|
|
116
|
+
"""
|
|
117
|
+
self._db = mongo_db
|
|
118
|
+
self._collection = mongo_db[AUDIT_COLLECTION]
|
|
119
|
+
self._retention_days = retention_days
|
|
120
|
+
self._indexes_created = False
|
|
121
|
+
|
|
122
|
+
logger.info(f"AuthAuditLog initialized (retention: {retention_days} days)")
|
|
123
|
+
|
|
124
|
+
async def ensure_indexes(self) -> None:
|
|
125
|
+
"""Create necessary indexes for the audit collection."""
|
|
126
|
+
if self._indexes_created:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
# Index for querying by user
|
|
131
|
+
await self._collection.create_index(
|
|
132
|
+
[("user_email", 1), ("timestamp", -1)], name="user_email_timestamp_idx"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Index for querying by action type
|
|
136
|
+
await self._collection.create_index(
|
|
137
|
+
[("action", 1), ("timestamp", -1)], name="action_timestamp_idx"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Index for querying by IP address
|
|
141
|
+
await self._collection.create_index(
|
|
142
|
+
[("ip_address", 1), ("timestamp", -1)], name="ip_timestamp_idx"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Index for querying by app
|
|
146
|
+
await self._collection.create_index(
|
|
147
|
+
[("app_slug", 1), ("timestamp", -1)], name="app_timestamp_idx"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# TTL index for automatic cleanup
|
|
151
|
+
await self._collection.create_index(
|
|
152
|
+
"expires_at", expireAfterSeconds=0, name="expires_at_ttl_idx"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
self._indexes_created = True
|
|
156
|
+
logger.info("AuthAuditLog indexes ensured")
|
|
157
|
+
except OperationFailure as e:
|
|
158
|
+
logger.warning(f"Failed to create audit indexes: {e}")
|
|
159
|
+
|
|
160
|
+
async def log_event(
|
|
161
|
+
self,
|
|
162
|
+
action: AuthAction,
|
|
163
|
+
success: bool,
|
|
164
|
+
user_email: Optional[str] = None,
|
|
165
|
+
user_id: Optional[str] = None,
|
|
166
|
+
app_slug: Optional[str] = None,
|
|
167
|
+
ip_address: Optional[str] = None,
|
|
168
|
+
user_agent: Optional[str] = None,
|
|
169
|
+
details: Optional[Dict[str, Any]] = None,
|
|
170
|
+
) -> str:
|
|
171
|
+
"""
|
|
172
|
+
Log an authentication event.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
action: Type of authentication action
|
|
176
|
+
success: Whether the action succeeded
|
|
177
|
+
user_email: User's email address
|
|
178
|
+
user_id: User's ID
|
|
179
|
+
app_slug: App where the action occurred
|
|
180
|
+
ip_address: Client IP address
|
|
181
|
+
user_agent: Client user agent string
|
|
182
|
+
details: Additional details (e.g., failure reason)
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Inserted document ID as string
|
|
186
|
+
"""
|
|
187
|
+
await self.ensure_indexes()
|
|
188
|
+
|
|
189
|
+
now = datetime.utcnow()
|
|
190
|
+
expires_at = now + timedelta(days=self._retention_days)
|
|
191
|
+
|
|
192
|
+
doc = {
|
|
193
|
+
"timestamp": now,
|
|
194
|
+
"action": action.value if isinstance(action, AuthAction) else action,
|
|
195
|
+
"success": success,
|
|
196
|
+
"user_email": user_email,
|
|
197
|
+
"user_id": user_id,
|
|
198
|
+
"app_slug": app_slug,
|
|
199
|
+
"ip_address": ip_address,
|
|
200
|
+
"user_agent": user_agent,
|
|
201
|
+
"details": details,
|
|
202
|
+
"expires_at": expires_at,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
result = await self._collection.insert_one(doc)
|
|
206
|
+
|
|
207
|
+
# Log to application logger as well
|
|
208
|
+
log_level = logging.INFO if success else logging.WARNING
|
|
209
|
+
logger.log(
|
|
210
|
+
log_level,
|
|
211
|
+
f"AUTH_AUDIT: {action.value if isinstance(action, AuthAction) else action} "
|
|
212
|
+
f"success={success} email={user_email} ip={ip_address} app={app_slug}",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return str(result.inserted_id)
|
|
216
|
+
|
|
217
|
+
async def log_login_success(
|
|
218
|
+
self,
|
|
219
|
+
email: str,
|
|
220
|
+
ip_address: Optional[str] = None,
|
|
221
|
+
user_agent: Optional[str] = None,
|
|
222
|
+
app_slug: Optional[str] = None,
|
|
223
|
+
) -> str:
|
|
224
|
+
"""Convenience method to log successful login."""
|
|
225
|
+
return await self.log_event(
|
|
226
|
+
action=AuthAction.LOGIN_SUCCESS,
|
|
227
|
+
success=True,
|
|
228
|
+
user_email=email,
|
|
229
|
+
ip_address=ip_address,
|
|
230
|
+
user_agent=user_agent,
|
|
231
|
+
app_slug=app_slug,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
async def log_login_failed(
|
|
235
|
+
self,
|
|
236
|
+
email: str,
|
|
237
|
+
reason: str = "invalid_credentials",
|
|
238
|
+
ip_address: Optional[str] = None,
|
|
239
|
+
user_agent: Optional[str] = None,
|
|
240
|
+
app_slug: Optional[str] = None,
|
|
241
|
+
) -> str:
|
|
242
|
+
"""Convenience method to log failed login."""
|
|
243
|
+
return await self.log_event(
|
|
244
|
+
action=AuthAction.LOGIN_FAILED,
|
|
245
|
+
success=False,
|
|
246
|
+
user_email=email,
|
|
247
|
+
ip_address=ip_address,
|
|
248
|
+
user_agent=user_agent,
|
|
249
|
+
app_slug=app_slug,
|
|
250
|
+
details={"reason": reason},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
async def log_logout(
|
|
254
|
+
self,
|
|
255
|
+
email: str,
|
|
256
|
+
ip_address: Optional[str] = None,
|
|
257
|
+
app_slug: Optional[str] = None,
|
|
258
|
+
) -> str:
|
|
259
|
+
"""Convenience method to log logout."""
|
|
260
|
+
return await self.log_event(
|
|
261
|
+
action=AuthAction.LOGOUT,
|
|
262
|
+
success=True,
|
|
263
|
+
user_email=email,
|
|
264
|
+
ip_address=ip_address,
|
|
265
|
+
app_slug=app_slug,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
async def log_register(
|
|
269
|
+
self,
|
|
270
|
+
email: str,
|
|
271
|
+
ip_address: Optional[str] = None,
|
|
272
|
+
user_agent: Optional[str] = None,
|
|
273
|
+
app_slug: Optional[str] = None,
|
|
274
|
+
) -> str:
|
|
275
|
+
"""Convenience method to log new user registration."""
|
|
276
|
+
return await self.log_event(
|
|
277
|
+
action=AuthAction.REGISTER,
|
|
278
|
+
success=True,
|
|
279
|
+
user_email=email,
|
|
280
|
+
ip_address=ip_address,
|
|
281
|
+
user_agent=user_agent,
|
|
282
|
+
app_slug=app_slug,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
async def log_role_change(
|
|
286
|
+
self,
|
|
287
|
+
email: str,
|
|
288
|
+
app_slug: str,
|
|
289
|
+
old_roles: List[str],
|
|
290
|
+
new_roles: List[str],
|
|
291
|
+
changed_by: Optional[str] = None,
|
|
292
|
+
ip_address: Optional[str] = None,
|
|
293
|
+
) -> str:
|
|
294
|
+
"""Log a role change event."""
|
|
295
|
+
action = (
|
|
296
|
+
AuthAction.ROLE_GRANTED if len(new_roles) > len(old_roles) else AuthAction.ROLE_REVOKED
|
|
297
|
+
)
|
|
298
|
+
return await self.log_event(
|
|
299
|
+
action=action,
|
|
300
|
+
success=True,
|
|
301
|
+
user_email=email,
|
|
302
|
+
app_slug=app_slug,
|
|
303
|
+
ip_address=ip_address,
|
|
304
|
+
details={
|
|
305
|
+
"old_roles": old_roles,
|
|
306
|
+
"new_roles": new_roles,
|
|
307
|
+
"changed_by": changed_by,
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
async def log_token_revoked(
|
|
312
|
+
self,
|
|
313
|
+
email: str,
|
|
314
|
+
reason: str = "logout",
|
|
315
|
+
ip_address: Optional[str] = None,
|
|
316
|
+
app_slug: Optional[str] = None,
|
|
317
|
+
) -> str:
|
|
318
|
+
"""Log token revocation."""
|
|
319
|
+
return await self.log_event(
|
|
320
|
+
action=AuthAction.TOKEN_REVOKED,
|
|
321
|
+
success=True,
|
|
322
|
+
user_email=email,
|
|
323
|
+
ip_address=ip_address,
|
|
324
|
+
app_slug=app_slug,
|
|
325
|
+
details={"reason": reason},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
async def log_rate_limit_exceeded(
|
|
329
|
+
self,
|
|
330
|
+
ip_address: str,
|
|
331
|
+
endpoint: str,
|
|
332
|
+
email: Optional[str] = None,
|
|
333
|
+
app_slug: Optional[str] = None,
|
|
334
|
+
) -> str:
|
|
335
|
+
"""Log rate limit exceeded event."""
|
|
336
|
+
return await self.log_event(
|
|
337
|
+
action=AuthAction.RATE_LIMIT_EXCEEDED,
|
|
338
|
+
success=False,
|
|
339
|
+
user_email=email,
|
|
340
|
+
ip_address=ip_address,
|
|
341
|
+
app_slug=app_slug,
|
|
342
|
+
details={"endpoint": endpoint},
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# ==========================================================================
|
|
346
|
+
# Query Methods for Security Analysis
|
|
347
|
+
# ==========================================================================
|
|
348
|
+
|
|
349
|
+
async def get_recent_events(
|
|
350
|
+
self,
|
|
351
|
+
hours: int = 24,
|
|
352
|
+
action: Optional[AuthAction] = None,
|
|
353
|
+
success: Optional[bool] = None,
|
|
354
|
+
limit: int = 100,
|
|
355
|
+
) -> List[Dict[str, Any]]:
|
|
356
|
+
"""
|
|
357
|
+
Get recent audit events.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
hours: How many hours back to search
|
|
361
|
+
action: Filter by action type
|
|
362
|
+
success: Filter by success status
|
|
363
|
+
limit: Maximum results to return
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
List of audit event documents
|
|
367
|
+
"""
|
|
368
|
+
since = datetime.utcnow() - timedelta(hours=hours)
|
|
369
|
+
|
|
370
|
+
query: Dict[str, Any] = {"timestamp": {"$gte": since}}
|
|
371
|
+
if action:
|
|
372
|
+
query["action"] = action.value if isinstance(action, AuthAction) else action
|
|
373
|
+
if success is not None:
|
|
374
|
+
query["success"] = success
|
|
375
|
+
|
|
376
|
+
cursor = self._collection.find(query).sort("timestamp", -1).limit(limit)
|
|
377
|
+
events = await cursor.to_list(length=limit)
|
|
378
|
+
|
|
379
|
+
# Convert ObjectIds to strings
|
|
380
|
+
for event in events:
|
|
381
|
+
event["_id"] = str(event["_id"])
|
|
382
|
+
|
|
383
|
+
return events
|
|
384
|
+
|
|
385
|
+
async def get_failed_logins(
|
|
386
|
+
self,
|
|
387
|
+
email: Optional[str] = None,
|
|
388
|
+
ip_address: Optional[str] = None,
|
|
389
|
+
hours: int = 24,
|
|
390
|
+
limit: int = 100,
|
|
391
|
+
) -> List[Dict[str, Any]]:
|
|
392
|
+
"""
|
|
393
|
+
Get failed login attempts.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
email: Filter by email
|
|
397
|
+
ip_address: Filter by IP address
|
|
398
|
+
hours: How many hours back to search
|
|
399
|
+
limit: Maximum results
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
List of failed login events
|
|
403
|
+
"""
|
|
404
|
+
since = datetime.utcnow() - timedelta(hours=hours)
|
|
405
|
+
|
|
406
|
+
query: Dict[str, Any] = {
|
|
407
|
+
"action": AuthAction.LOGIN_FAILED.value,
|
|
408
|
+
"timestamp": {"$gte": since},
|
|
409
|
+
}
|
|
410
|
+
if email:
|
|
411
|
+
query["user_email"] = email
|
|
412
|
+
if ip_address:
|
|
413
|
+
query["ip_address"] = ip_address
|
|
414
|
+
|
|
415
|
+
cursor = self._collection.find(query).sort("timestamp", -1).limit(limit)
|
|
416
|
+
events = await cursor.to_list(length=limit)
|
|
417
|
+
|
|
418
|
+
for event in events:
|
|
419
|
+
event["_id"] = str(event["_id"])
|
|
420
|
+
|
|
421
|
+
return events
|
|
422
|
+
|
|
423
|
+
async def get_user_activity(
|
|
424
|
+
self,
|
|
425
|
+
email: str,
|
|
426
|
+
hours: int = 168, # 7 days
|
|
427
|
+
limit: int = 100,
|
|
428
|
+
) -> List[Dict[str, Any]]:
|
|
429
|
+
"""
|
|
430
|
+
Get all activity for a specific user.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
email: User email
|
|
434
|
+
hours: How many hours back to search
|
|
435
|
+
limit: Maximum results
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
List of user's audit events
|
|
439
|
+
"""
|
|
440
|
+
since = datetime.utcnow() - timedelta(hours=hours)
|
|
441
|
+
|
|
442
|
+
cursor = (
|
|
443
|
+
self._collection.find(
|
|
444
|
+
{
|
|
445
|
+
"user_email": email,
|
|
446
|
+
"timestamp": {"$gte": since},
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
.sort("timestamp", -1)
|
|
450
|
+
.limit(limit)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
events = await cursor.to_list(length=limit)
|
|
454
|
+
|
|
455
|
+
for event in events:
|
|
456
|
+
event["_id"] = str(event["_id"])
|
|
457
|
+
|
|
458
|
+
return events
|
|
459
|
+
|
|
460
|
+
async def get_ip_activity(
|
|
461
|
+
self,
|
|
462
|
+
ip_address: str,
|
|
463
|
+
hours: int = 24,
|
|
464
|
+
limit: int = 100,
|
|
465
|
+
) -> List[Dict[str, Any]]:
|
|
466
|
+
"""
|
|
467
|
+
Get all activity from a specific IP address.
|
|
468
|
+
|
|
469
|
+
Useful for investigating suspicious activity.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
ip_address: IP address to search
|
|
473
|
+
hours: How many hours back
|
|
474
|
+
limit: Maximum results
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
List of audit events from that IP
|
|
478
|
+
"""
|
|
479
|
+
since = datetime.utcnow() - timedelta(hours=hours)
|
|
480
|
+
|
|
481
|
+
cursor = (
|
|
482
|
+
self._collection.find(
|
|
483
|
+
{
|
|
484
|
+
"ip_address": ip_address,
|
|
485
|
+
"timestamp": {"$gte": since},
|
|
486
|
+
}
|
|
487
|
+
)
|
|
488
|
+
.sort("timestamp", -1)
|
|
489
|
+
.limit(limit)
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
events = await cursor.to_list(length=limit)
|
|
493
|
+
|
|
494
|
+
for event in events:
|
|
495
|
+
event["_id"] = str(event["_id"])
|
|
496
|
+
|
|
497
|
+
return events
|
|
498
|
+
|
|
499
|
+
async def count_failed_logins(
|
|
500
|
+
self,
|
|
501
|
+
email: Optional[str] = None,
|
|
502
|
+
ip_address: Optional[str] = None,
|
|
503
|
+
hours: int = 1,
|
|
504
|
+
) -> int:
|
|
505
|
+
"""
|
|
506
|
+
Count failed login attempts in a time window.
|
|
507
|
+
|
|
508
|
+
Useful for detecting brute-force attacks.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
email: Filter by email
|
|
512
|
+
ip_address: Filter by IP
|
|
513
|
+
hours: Time window
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Number of failed login attempts
|
|
517
|
+
"""
|
|
518
|
+
since = datetime.utcnow() - timedelta(hours=hours)
|
|
519
|
+
|
|
520
|
+
query: Dict[str, Any] = {
|
|
521
|
+
"action": AuthAction.LOGIN_FAILED.value,
|
|
522
|
+
"timestamp": {"$gte": since},
|
|
523
|
+
}
|
|
524
|
+
if email:
|
|
525
|
+
query["user_email"] = email
|
|
526
|
+
if ip_address:
|
|
527
|
+
query["ip_address"] = ip_address
|
|
528
|
+
|
|
529
|
+
return await self._collection.count_documents(query)
|
|
530
|
+
|
|
531
|
+
async def get_security_summary(
|
|
532
|
+
self,
|
|
533
|
+
hours: int = 24,
|
|
534
|
+
) -> Dict[str, Any]:
|
|
535
|
+
"""
|
|
536
|
+
Get security summary statistics.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
hours: Time window for statistics
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
Dict with security metrics
|
|
543
|
+
"""
|
|
544
|
+
since = datetime.utcnow() - timedelta(hours=hours)
|
|
545
|
+
|
|
546
|
+
# Use aggregation for efficient counting
|
|
547
|
+
pipeline = [
|
|
548
|
+
{"$match": {"timestamp": {"$gte": since}}},
|
|
549
|
+
{
|
|
550
|
+
"$group": {
|
|
551
|
+
"_id": {"action": "$action", "success": "$success"},
|
|
552
|
+
"count": {"$sum": 1},
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
]
|
|
556
|
+
|
|
557
|
+
cursor = self._collection.aggregate(pipeline)
|
|
558
|
+
results = await cursor.to_list(length=100)
|
|
559
|
+
|
|
560
|
+
# Build summary
|
|
561
|
+
summary = {
|
|
562
|
+
"period_hours": hours,
|
|
563
|
+
"total_events": 0,
|
|
564
|
+
"login_success": 0,
|
|
565
|
+
"login_failed": 0,
|
|
566
|
+
"registrations": 0,
|
|
567
|
+
"logouts": 0,
|
|
568
|
+
"tokens_revoked": 0,
|
|
569
|
+
"rate_limits_exceeded": 0,
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
for result in results:
|
|
573
|
+
action = result["_id"]["action"]
|
|
574
|
+
_success = result["_id"]["success"] # noqa: F841 - extracted for potential future use
|
|
575
|
+
count = result["count"]
|
|
576
|
+
|
|
577
|
+
summary["total_events"] += count
|
|
578
|
+
|
|
579
|
+
if action == AuthAction.LOGIN_SUCCESS.value:
|
|
580
|
+
summary["login_success"] = count
|
|
581
|
+
elif action == AuthAction.LOGIN_FAILED.value:
|
|
582
|
+
summary["login_failed"] = count
|
|
583
|
+
elif action == AuthAction.REGISTER.value:
|
|
584
|
+
summary["registrations"] = count
|
|
585
|
+
elif action == AuthAction.LOGOUT.value:
|
|
586
|
+
summary["logouts"] = count
|
|
587
|
+
elif action == AuthAction.TOKEN_REVOKED.value:
|
|
588
|
+
summary["tokens_revoked"] = count
|
|
589
|
+
elif action == AuthAction.RATE_LIMIT_EXCEEDED.value:
|
|
590
|
+
summary["rate_limits_exceeded"] = count
|
|
591
|
+
|
|
592
|
+
return summary
|
|
@@ -11,7 +11,7 @@ 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, Optional
|
|
15
15
|
|
|
16
16
|
from .casbin_models import DEFAULT_RBAC_MODEL, SIMPLE_ACL_MODEL
|
|
17
17
|
|
|
@@ -43,9 +43,7 @@ def get_casbin_model(model_type: str = "rbac") -> str:
|
|
|
43
43
|
if model_path.exists():
|
|
44
44
|
return model_path.read_text()
|
|
45
45
|
else:
|
|
46
|
-
logger.warning(
|
|
47
|
-
f"Casbin model file not found: {model_type}, using default RBAC model"
|
|
48
|
-
)
|
|
46
|
+
logger.warning(f"Casbin model file not found: {model_type}, using default RBAC model")
|
|
49
47
|
return DEFAULT_RBAC_MODEL
|
|
50
48
|
|
|
51
49
|
|
|
@@ -59,9 +57,9 @@ async def create_casbin_enforcer(
|
|
|
59
57
|
Create a Casbin AsyncEnforcer with MongoDB adapter.
|
|
60
58
|
|
|
61
59
|
Args:
|
|
62
|
-
db: MongoDB database instance (
|
|
60
|
+
db: Scoped MongoDB database instance (ScopedMongoWrapper)
|
|
63
61
|
model: Casbin model type ("rbac", "acl") or path to model file
|
|
64
|
-
policies_collection: MongoDB collection name for policies
|
|
62
|
+
policies_collection: MongoDB collection name for policies (will be app-scoped)
|
|
65
63
|
default_roles: List of default roles to create (optional)
|
|
66
64
|
|
|
67
65
|
Returns:
|
|
@@ -104,7 +102,7 @@ async def create_casbin_enforcer(
|
|
|
104
102
|
return enforcer
|
|
105
103
|
|
|
106
104
|
|
|
107
|
-
async def _create_default_roles(enforcer:
|
|
105
|
+
async def _create_default_roles(enforcer: casbin.AsyncEnforcer, roles: list) -> None:
|
|
108
106
|
"""
|
|
109
107
|
Create default roles in Casbin (as grouping rules).
|
|
110
108
|
|
|
@@ -129,8 +127,8 @@ async def _create_default_roles(enforcer: "casbin.AsyncEnforcer", roles: list) -
|
|
|
129
127
|
|
|
130
128
|
|
|
131
129
|
async def initialize_casbin_from_manifest(
|
|
132
|
-
engine, app_slug: str, auth_config:
|
|
133
|
-
) -> Optional[
|
|
130
|
+
engine, app_slug: str, auth_config: dict[str, Any]
|
|
131
|
+
) -> Optional[CasbinAdapter]:
|
|
134
132
|
"""
|
|
135
133
|
Initialize Casbin provider from manifest configuration.
|
|
136
134
|
|
|
@@ -155,13 +153,11 @@ async def initialize_casbin_from_manifest(
|
|
|
155
153
|
# Get authorization config
|
|
156
154
|
authorization = auth_policy.get("authorization", {})
|
|
157
155
|
model = authorization.get("model", "rbac")
|
|
158
|
-
policies_collection = authorization.get(
|
|
159
|
-
"policies_collection", "casbin_policies"
|
|
160
|
-
)
|
|
156
|
+
policies_collection = authorization.get("policies_collection", "casbin_policies")
|
|
161
157
|
default_roles = authorization.get("default_roles", [])
|
|
162
158
|
|
|
163
|
-
# Get database from engine
|
|
164
|
-
db = engine.
|
|
159
|
+
# Get scoped database from engine
|
|
160
|
+
db = engine.get_scoped_db(app_slug)
|
|
165
161
|
|
|
166
162
|
# Create enforcer
|
|
167
163
|
enforcer = await create_casbin_enforcer(
|
|
@@ -12,9 +12,12 @@ from typing import Any, Dict
|
|
|
12
12
|
|
|
13
13
|
from fastapi import Request
|
|
14
14
|
|
|
15
|
-
from .config_defaults import (
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
from .config_defaults import (
|
|
16
|
+
CORS_DEFAULTS,
|
|
17
|
+
OBSERVABILITY_DEFAULTS,
|
|
18
|
+
SECURITY_CONFIG_DEFAULTS,
|
|
19
|
+
TOKEN_MANAGEMENT_DEFAULTS,
|
|
20
|
+
)
|
|
18
21
|
|
|
19
22
|
logger = logging.getLogger(__name__)
|
|
20
23
|
|
|
@@ -132,9 +135,7 @@ def get_ip_validation_config(request: Request) -> Dict[str, Any]:
|
|
|
132
135
|
IP validation configuration dictionary
|
|
133
136
|
"""
|
|
134
137
|
security_config = get_security_config(request)
|
|
135
|
-
return security_config.get(
|
|
136
|
-
"ip_validation", SECURITY_CONFIG_DEFAULTS["ip_validation"].copy()
|
|
137
|
-
)
|
|
138
|
+
return security_config.get("ip_validation", SECURITY_CONFIG_DEFAULTS["ip_validation"].copy())
|
|
138
139
|
|
|
139
140
|
|
|
140
141
|
def get_token_fingerprinting_config(request: Request) -> Dict[str, Any]:
|