mdb-engine 0.1.6__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 +104 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +648 -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 +264 -69
- 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 +47 -50
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +53 -80
- mdb_engine/auth/jwt.py +2 -6
- mdb_engine/auth/middleware.py +77 -34
- mdb_engine/auth/oso_factory.py +18 -38
- mdb_engine/auth/provider.py +270 -171
- 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 +1057 -93
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +459 -150
- 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 +140 -17
- 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/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 +31 -24
- mdb_engine/embeddings/dependencies.py +37 -154
- 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/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 +25 -60
- mdb_engine-0.2.0.dist-info/METADATA +313 -0
- mdb_engine-0.2.0.dist-info/RECORD +96 -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.2.0.dist-info}/WHEEL +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.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
|