feat: implement audit logging middleware for sensitive API operations (#183)
feat: implement audit logging middleware for sensitive API operations
This commit is contained in:
@@ -69,7 +69,9 @@ async def get_current_user(
|
|||||||
token: str | None = None
|
token: str | None = None
|
||||||
|
|
||||||
# 1. Check session cookie — prefer __Secure- variant (HTTPS) over plain (HTTP dev)
|
# 1. Check session cookie — prefer __Secure- variant (HTTPS) over plain (HTTP dev)
|
||||||
cookie_token = request.cookies.get(SECURE_SESSION_COOKIE_NAME) or request.cookies.get(SESSION_COOKIE_NAME)
|
cookie_token = request.cookies.get(SECURE_SESSION_COOKIE_NAME) or request.cookies.get(
|
||||||
|
SESSION_COOKIE_NAME
|
||||||
|
)
|
||||||
if cookie_token:
|
if cookie_token:
|
||||||
# Better-Auth cookie format is "token.sessionId" — extract just the token part
|
# Better-Auth cookie format is "token.sessionId" — extract just the token part
|
||||||
token = cookie_token.split(".")[0] if "." in cookie_token else cookie_token
|
token = cookie_token.split(".")[0] if "." in cookie_token else cookie_token
|
||||||
@@ -86,7 +88,9 @@ async def get_current_user(
|
|||||||
detail="Authentication required",
|
detail="Authentication required",
|
||||||
)
|
)
|
||||||
|
|
||||||
return await _validate_session_token(token, db)
|
user_id = await _validate_session_token(token, db)
|
||||||
|
request.state.user_id = user_id
|
||||||
|
return user_id
|
||||||
|
|
||||||
|
|
||||||
async def verify_service_key(x_service_key: str = Header()) -> None:
|
async def verify_service_key(x_service_key: str = Header()) -> None:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from cartsnitch_api.database import dispose_engine
|
|||||||
from cartsnitch_api.middleware.cors import add_cors_middleware
|
from cartsnitch_api.middleware.cors import add_cors_middleware
|
||||||
from cartsnitch_api.middleware.error_handler import add_error_handlers, add_error_monitor_middleware
|
from cartsnitch_api.middleware.error_handler import add_error_handlers, add_error_monitor_middleware
|
||||||
from cartsnitch_api.middleware.rate_limit import add_rate_limit_middleware
|
from cartsnitch_api.middleware.rate_limit import add_rate_limit_middleware
|
||||||
|
from cartsnitch_api.middleware.audit import add_audit_middleware
|
||||||
from cartsnitch_api.routes.alerts import router as alerts_router
|
from cartsnitch_api.routes.alerts import router as alerts_router
|
||||||
from cartsnitch_api.routes.coupons import router as coupons_router
|
from cartsnitch_api.routes.coupons import router as coupons_router
|
||||||
from cartsnitch_api.routes.health import router as health_router
|
from cartsnitch_api.routes.health import router as health_router
|
||||||
@@ -43,6 +44,7 @@ def create_app() -> FastAPI:
|
|||||||
add_cors_middleware(app)
|
add_cors_middleware(app)
|
||||||
add_error_monitor_middleware(app)
|
add_error_monitor_middleware(app)
|
||||||
add_rate_limit_middleware(app)
|
add_rate_limit_middleware(app)
|
||||||
|
add_audit_middleware(app)
|
||||||
|
|
||||||
# Exception handlers
|
# Exception handlers
|
||||||
add_error_handlers(app)
|
add_error_handlers(app)
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Audit logging middleware for sensitive API operations.
|
||||||
|
|
||||||
|
Logs structured JSON for POST/PUT/PATCH/DELETE requests and GET /auth/me.
|
||||||
|
Never logs request bodies, response bodies, Authorization headers, or cookie values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
|
||||||
|
logger = logging.getLogger("cartsnitch_api.audit")
|
||||||
|
|
||||||
|
HEALTH_PATHS = {"/health", "/healthz", "/ready"}
|
||||||
|
|
||||||
|
|
||||||
|
class AuditMiddleware(BaseHTTPMiddleware):
|
||||||
|
"""Middleware to log structured audit events for sensitive operations."""
|
||||||
|
|
||||||
|
async def dispatch(
|
||||||
|
self,
|
||||||
|
request: Request,
|
||||||
|
call_next: Callable[[Request], Awaitable],
|
||||||
|
):
|
||||||
|
if request.method == "OPTIONS" or request.url.path in HEALTH_PATHS:
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
method = request.method
|
||||||
|
path = request.url.path
|
||||||
|
|
||||||
|
is_sensitive_write = method in {"POST", "PUT", "PATCH", "DELETE"}
|
||||||
|
is_auth_me_read = method == "GET" and path == "/auth/me"
|
||||||
|
|
||||||
|
if not (is_sensitive_write or is_auth_me_read):
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
response = await call_next(request)
|
||||||
|
duration_ms = (time.perf_counter() - start) * 1000
|
||||||
|
|
||||||
|
user_id = getattr(request.state, "user_id", None)
|
||||||
|
client_ip = request.client.host if request.client else "unknown"
|
||||||
|
|
||||||
|
log_entry = {
|
||||||
|
"event": "audit",
|
||||||
|
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"user_id": user_id,
|
||||||
|
"method": method,
|
||||||
|
"path": path,
|
||||||
|
"client_ip": client_ip,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"duration_ms": round(duration_ms, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(json.dumps(log_entry))
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def add_audit_middleware(app: FastAPI) -> None:
|
||||||
|
app.add_middleware(AuditMiddleware)
|
||||||
Reference in New Issue
Block a user