Spaces:
Sleeping
Sleeping
log
Browse files- core/security.py +0 -32
- routers/auth.py +20 -16
- services/audit_service.py +82 -0
core/security.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Core Security Utilities
|
| 3 |
-
|
| 4 |
-
Note: Secret key authentication has been replaced with Google OAuth.
|
| 5 |
-
The bcrypt functions below are kept for potential future use (e.g., admin passwords).
|
| 6 |
-
"""
|
| 7 |
-
import bcrypt
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 11 |
-
"""
|
| 12 |
-
Verify a password against a bcrypt hash.
|
| 13 |
-
|
| 14 |
-
Note: This is no longer used for user authentication (moved to Google OAuth).
|
| 15 |
-
Kept for potential admin/internal use cases.
|
| 16 |
-
"""
|
| 17 |
-
if isinstance(hashed_password, str):
|
| 18 |
-
hashed_password = hashed_password.encode('utf-8')
|
| 19 |
-
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password)
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def get_password_hash(password: str) -> str:
|
| 23 |
-
"""
|
| 24 |
-
Hash a password using bcrypt.
|
| 25 |
-
|
| 26 |
-
Note: This is no longer used for user authentication (moved to Google OAuth).
|
| 27 |
-
Kept for potential admin/internal use cases.
|
| 28 |
-
"""
|
| 29 |
-
salt = bcrypt.gensalt(rounds=12)
|
| 30 |
-
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
|
| 31 |
-
return hashed.decode('utf-8')
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
routers/auth.py
CHANGED
|
@@ -25,14 +25,19 @@ from core.schemas import (
|
|
| 25 |
from services.auth_service.google_provider import (
|
| 26 |
GoogleAuthService,
|
| 27 |
GoogleUserInfo,
|
| 28 |
-
InvalidTokenError,
|
|
|
|
|
|
|
| 29 |
)
|
| 30 |
from services.auth_service.jwt_provider import (
|
| 31 |
JWTService,
|
| 32 |
create_access_token,
|
|
|
|
|
|
|
| 33 |
)
|
| 34 |
from dependencies import check_rate_limit, get_current_user
|
| 35 |
from services.drive_service import DriveService
|
|
|
|
| 36 |
|
| 37 |
logger = logging.getLogger(__name__)
|
| 38 |
|
|
@@ -105,15 +110,14 @@ async def google_auth(
|
|
| 105 |
logger.warning(f"Invalid Google token from {ip}: {e}")
|
| 106 |
|
| 107 |
# Log failed attempt
|
| 108 |
-
|
|
|
|
| 109 |
log_type="server",
|
| 110 |
-
user_id=None,
|
| 111 |
action="google_auth",
|
| 112 |
-
ip_address=ip,
|
| 113 |
status="failed",
|
| 114 |
-
error_message=str(e)
|
|
|
|
| 115 |
)
|
| 116 |
-
db.add(audit_log)
|
| 117 |
await db.commit()
|
| 118 |
|
| 119 |
raise HTTPException(
|
|
@@ -185,15 +189,15 @@ async def google_auth(
|
|
| 185 |
db.add(client_user)
|
| 186 |
|
| 187 |
# Log successful auth
|
| 188 |
-
|
|
|
|
| 189 |
log_type="server",
|
| 190 |
-
user_id=user.id,
|
| 191 |
client_user_id=request.temp_user_id,
|
| 192 |
action="google_auth",
|
| 193 |
-
|
| 194 |
-
|
| 195 |
)
|
| 196 |
-
db.add(audit_log)
|
| 197 |
await db.commit()
|
| 198 |
|
| 199 |
# Create our JWT access token with current token_version
|
|
@@ -327,14 +331,14 @@ async def logout(
|
|
| 327 |
logger.info(f"User {user.user_id} logged out. Token version incremented to {user.token_version}")
|
| 328 |
|
| 329 |
# Log logout
|
| 330 |
-
|
|
|
|
| 331 |
log_type="server",
|
| 332 |
-
user_id=user.id,
|
| 333 |
action="logout",
|
| 334 |
-
|
| 335 |
-
|
| 336 |
)
|
| 337 |
-
db.add(audit_log)
|
| 338 |
await db.commit()
|
| 339 |
|
| 340 |
# Sync DB to Drive (Async)
|
|
|
|
| 25 |
from services.auth_service.google_provider import (
|
| 26 |
GoogleAuthService,
|
| 27 |
GoogleUserInfo,
|
| 28 |
+
InvalidTokenError as GoogleInvalidTokenError,
|
| 29 |
+
ConfigurationError as GoogleConfigError,
|
| 30 |
+
get_google_auth_service,
|
| 31 |
)
|
| 32 |
from services.auth_service.jwt_provider import (
|
| 33 |
JWTService,
|
| 34 |
create_access_token,
|
| 35 |
+
get_jwt_service,
|
| 36 |
+
InvalidTokenError as JWTInvalidTokenError,
|
| 37 |
)
|
| 38 |
from dependencies import check_rate_limit, get_current_user
|
| 39 |
from services.drive_service import DriveService
|
| 40 |
+
from services.audit_service import AuditService
|
| 41 |
|
| 42 |
logger = logging.getLogger(__name__)
|
| 43 |
|
|
|
|
| 110 |
logger.warning(f"Invalid Google token from {ip}: {e}")
|
| 111 |
|
| 112 |
# Log failed attempt
|
| 113 |
+
await AuditService.log_event(
|
| 114 |
+
db=db,
|
| 115 |
log_type="server",
|
|
|
|
| 116 |
action="google_auth",
|
|
|
|
| 117 |
status="failed",
|
| 118 |
+
error_message=str(e),
|
| 119 |
+
request=req
|
| 120 |
)
|
|
|
|
| 121 |
await db.commit()
|
| 122 |
|
| 123 |
raise HTTPException(
|
|
|
|
| 189 |
db.add(client_user)
|
| 190 |
|
| 191 |
# Log successful auth
|
| 192 |
+
await AuditService.log_event(
|
| 193 |
+
db=db,
|
| 194 |
log_type="server",
|
| 195 |
+
user_id=user.id,
|
| 196 |
client_user_id=request.temp_user_id,
|
| 197 |
action="google_auth",
|
| 198 |
+
status="success",
|
| 199 |
+
request=req
|
| 200 |
)
|
|
|
|
| 201 |
await db.commit()
|
| 202 |
|
| 203 |
# Create our JWT access token with current token_version
|
|
|
|
| 331 |
logger.info(f"User {user.user_id} logged out. Token version incremented to {user.token_version}")
|
| 332 |
|
| 333 |
# Log logout
|
| 334 |
+
await AuditService.log_event(
|
| 335 |
+
db=db,
|
| 336 |
log_type="server",
|
| 337 |
+
user_id=user.id,
|
| 338 |
action="logout",
|
| 339 |
+
status="success",
|
| 340 |
+
request=req
|
| 341 |
)
|
|
|
|
| 342 |
await db.commit()
|
| 343 |
|
| 344 |
# Sync DB to Drive (Async)
|
services/audit_service.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Audit Service - Handle security and activity logging.
|
| 3 |
+
"""
|
| 4 |
+
import logging
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from typing import Optional, Dict, Any
|
| 7 |
+
from fastapi import Request
|
| 8 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 9 |
+
from core.models import AuditLog
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AuditService:
|
| 15 |
+
"""
|
| 16 |
+
Service for creating audit logs.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
@staticmethod
|
| 20 |
+
async def log_event(
|
| 21 |
+
db: AsyncSession,
|
| 22 |
+
action: str,
|
| 23 |
+
status: str,
|
| 24 |
+
user_id: Optional[int] = None,
|
| 25 |
+
client_user_id: Optional[str] = None,
|
| 26 |
+
details: Optional[Dict[str, Any]] = None,
|
| 27 |
+
request: Optional[Request] = None,
|
| 28 |
+
error_message: Optional[str] = None,
|
| 29 |
+
log_type: str = "client"
|
| 30 |
+
) -> AuditLog:
|
| 31 |
+
"""
|
| 32 |
+
Create an audit log entry.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
db: Database session
|
| 36 |
+
action: Event action (e.g., "login", "logout")
|
| 37 |
+
status: Outcome (e.g., "success", "failure")
|
| 38 |
+
user_id: ID of the authenticated user (if any)
|
| 39 |
+
client_user_id: Client-side ID (if any)
|
| 40 |
+
details: Additional metadata
|
| 41 |
+
request: FastAPI request object (for IP/UA)
|
| 42 |
+
error_message: Error description if failed
|
| 43 |
+
log_type: "client" or "server"
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
The created AuditLog instance
|
| 47 |
+
"""
|
| 48 |
+
ip_address = None
|
| 49 |
+
user_agent = None
|
| 50 |
+
refer_url = None
|
| 51 |
+
|
| 52 |
+
if request:
|
| 53 |
+
ip_address = request.client.host if request.client else None
|
| 54 |
+
user_agent = request.headers.get("user-agent")
|
| 55 |
+
refer_url = request.headers.get("referer")
|
| 56 |
+
|
| 57 |
+
# Create log entry
|
| 58 |
+
audit_log = AuditLog(
|
| 59 |
+
log_type=log_type,
|
| 60 |
+
user_id=user_id,
|
| 61 |
+
client_user_id=client_user_id,
|
| 62 |
+
action=action,
|
| 63 |
+
details=details or {},
|
| 64 |
+
ip_address=ip_address,
|
| 65 |
+
user_agent=user_agent,
|
| 66 |
+
refer_url=refer_url,
|
| 67 |
+
status=status,
|
| 68 |
+
error_message=error_message,
|
| 69 |
+
timestamp=datetime.utcnow()
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
db.add(audit_log)
|
| 73 |
+
# We don't commit here to allow the caller to group with other transactions
|
| 74 |
+
# or commit explicitly.
|
| 75 |
+
|
| 76 |
+
logger.info(f"Audit Log: [{status}] {action} - User: {user_id or 'Anon'} - IP: {ip_address}")
|
| 77 |
+
|
| 78 |
+
return audit_log
|
| 79 |
+
|
| 80 |
+
# Global instance not strictly needed as it's a static method helper,
|
| 81 |
+
# but keeping pattern consistent if we add state later.
|
| 82 |
+
audit_service = AuditService()
|