feat(B-RL): Rate-Limiting Konsistenz — zentrale Policies für Auth/AI/Upload/Webhook
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
B-RL: Rate-Limiting auf zentrale check_rate_limit() umgestellt - forgejo_error_reporter: In-Memory → zentrale Redis-Rate-Limit - ai_proactive: eigene Redis-Logik → zentrale check_rate_limit() - agent_runner: DB-basiertes Limit bleibt (zählt echte Ausführungen) - RateLimitPolicy Enum (AUTH/AI/UPLOAD/WEBHOOK) + check_rate_limit_policy() - 8 neue config.py Settings für Policy-Limits - 12 Routes mit Policies versehen (login, password-reset, AI, uploads, webhooks) Tests: 20 Tests in test_rate_limit_policies.py — alle grün - Policies, check_rate_limit, reset, get_client_ip, forgejo, ai_proactive - Keine Regression: 116 bestehende Tests grün
This commit is contained in:
+13
-26
@@ -1,29 +1,26 @@
|
||||
"""Error logging endpoint — accepts frontend errors and logs them.
|
||||
|
||||
No auth required so errors can be logged even during logout.
|
||||
Rate-limited to 10 requests per minute per IP (simple in-memory implementation).
|
||||
Rate-limited to 10 requests per minute per IP via central check_rate_limit().
|
||||
Context data is sanitized to prevent leaking sensitive information.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import logging
|
||||
import re
|
||||
from collections import defaultdict, deque
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Request, Response, status
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/v1/errors", tags=["errors"])
|
||||
|
||||
# -- Simple in-memory rate limiter (10 req/min per IP) --
|
||||
RATE_LIMIT = 10 # max requests
|
||||
RATE_WINDOW = 60 # seconds
|
||||
_ip_requests: dict[str, deque[float]] = defaultdict(deque)
|
||||
# -- Rate limit constants (10 req/min per IP) --
|
||||
_RATE_LIMIT_MAX = 10
|
||||
_RATE_LIMIT_WINDOW = 60 # seconds
|
||||
|
||||
# -- Sensitive key patterns to strip from context --
|
||||
_SENSITIVE_PATTERNS = re.compile(
|
||||
@@ -33,22 +30,6 @@ _SENSITIVE_PATTERNS = re.compile(
|
||||
)
|
||||
|
||||
|
||||
def _is_rate_limited(client_ip: str) -> bool:
|
||||
"""Return True if the IP has exceeded the rate limit."""
|
||||
now = time.monotonic()
|
||||
dq = _ip_requests[client_ip]
|
||||
|
||||
# Remove timestamps outside the window
|
||||
while dq and now - dq[0] > RATE_WINDOW:
|
||||
dq.popleft()
|
||||
|
||||
if len(dq) >= RATE_LIMIT:
|
||||
return True
|
||||
|
||||
dq.append(now)
|
||||
return False
|
||||
|
||||
|
||||
def _sanitize_context(context: Any, max_depth: int = 3, _depth: int = 0) -> Any:
|
||||
"""Recursively remove sensitive keys and limit depth/size of context data."""
|
||||
if _depth > max_depth:
|
||||
@@ -82,11 +63,17 @@ class ErrorReport(BaseModel):
|
||||
@router.post("", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def report_error(error: ErrorReport, request: Request) -> Response:
|
||||
"""Log a frontend error. No auth required. Rate-limited per IP."""
|
||||
from app.core.rate_limit import get_client_ip
|
||||
from app.core.rate_limit import check_rate_limit, get_client_ip
|
||||
|
||||
client_ip = get_client_ip(request)
|
||||
|
||||
if _is_rate_limited(client_ip):
|
||||
try:
|
||||
await check_rate_limit(
|
||||
f"rate:errors:{client_ip}",
|
||||
_RATE_LIMIT_MAX,
|
||||
_RATE_LIMIT_WINDOW,
|
||||
)
|
||||
except HTTPException:
|
||||
return Response(status_code=status.HTTP_429_TOO_MANY_REQUESTS)
|
||||
|
||||
# Sanitize context to prevent leaking sensitive data
|
||||
|
||||
Reference in New Issue
Block a user