diff --git a/backend/app/main.py b/backend/app/main.py index e90e337..b03b7c3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -16,6 +16,8 @@ from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from prometheus_fastapi_instrumentator import Instrumentator +from slowapi import _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded from sqlalchemy import text as sql_text from sqlalchemy.exc import IntegrityError @@ -34,6 +36,7 @@ from app.observability import RequestContextMiddleware, setup_logging from app.routers import ROUTERS from app.services.quote_service import auto_expire_quotes from app.utils.errors import AppError +from app.utils.ratelimit import limiter setup_logging(settings.LOG_FORMAT) logger = logging.getLogger("procurement") @@ -131,6 +134,10 @@ app.add_middleware( ) app.add_middleware(RequestContextMiddleware) +# Rate limiting — login brute-force brake (edge rate limit lives in Traefik). +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + # Prometheus /metrics (requests, latency, errors) — mounted at app level, # outside the authenticated /api/v1 prefix; scrape target for the # monitoring stack. diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index b021078..2005e66 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -13,7 +13,7 @@ Error mapping (documented in docs/casdoor-auth.md): import logging import httpx -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Request from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession @@ -23,6 +23,7 @@ from app.db.engine import get_db from app.routers.users import _user_projects, user_to_dict from app.services import analytics from app.utils.errors import ServiceUnavailableError, UnauthorizedError +from app.utils.ratelimit import limiter logger = logging.getLogger("procurement.auth") @@ -35,7 +36,8 @@ class LoginRequest(BaseModel): @router.post("/login") -async def login(payload: LoginRequest, db: AsyncSession = Depends(get_db)): +@limiter.limit("10/minute", error_message="تلاش‌های ورود بیش از حد مجاز — کمی بعد دوباره امتحان کنید") +async def login(request: Request, payload: LoginRequest, db: AsyncSession = Depends(get_db)): """Exchange username/password for a Casdoor JWT (OAuth password grant).""" url = f"{settings.CASDOOR_ENDPOINT}/api/login/oauth/access_token" form = { diff --git a/backend/app/utils/ratelimit.py b/backend/app/utils/ratelimit.py new file mode 100644 index 0000000..9eba047 --- /dev/null +++ b/backend/app/utils/ratelimit.py @@ -0,0 +1,11 @@ +"""Shared rate limiter (slowapi) — in-memory, single-worker friendly. + +Login brute-force brake: the edge (Traefik) has a coarse per-IP limit for +the whole API host; this adds a tight limit on the credential endpoint +itself. Kept in its own module so routers can import it without a circular +import on app.main. +""" +from slowapi import Limiter +from slowapi.util import get_remote_address + +limiter = Limiter(key_func=get_remote_address) diff --git a/backend/requirements.txt b/backend/requirements.txt index 93f8b24..d02ba87 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -17,3 +17,6 @@ bcrypt>=4.1.0 # Observability — metrics + Sentry/GlitchTip exception capture prometheus-fastapi-instrumentator>=7.0.0 sentry-sdk[fastapi]>=2.0.0 + +# Rate limiting (login brute-force brake; edge limit lives in Traefik) +slowapi>=0.1.9 diff --git a/docker-compose.yml b/docker-compose.yml index 69da10f..750f071 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: retries: 10 restart: unless-stopped + mem_limit: 1g # ========================================================================== # Traefik v3 — public reverse proxy (replaces nginx as the edge) @@ -42,6 +43,7 @@ services: image: traefik:v3.6 container_name: procurement-traefik restart: unless-stopped + mem_limit: 128m command: - --providers.docker=true - --providers.docker.exposedbydefault=false @@ -103,6 +105,7 @@ services: - default restart: unless-stopped + mem_limit: 512m # MinIO Client (mc): waits for the server, creates the bucket, exits. # Use for ad-hoc ops too: docker compose run --rm minio-init ... @@ -163,6 +166,7 @@ services: done " restart: unless-stopped + mem_limit: 256m # ========================================================================== # v2.6 — Casdoor (OIDC identity provider) @@ -201,6 +205,7 @@ services: - default restart: unless-stopped + mem_limit: 768m api: build: @@ -272,6 +277,7 @@ services: condition: service_completed_successfully restart: unless-stopped + mem_limit: 768m frontend: build: @@ -300,6 +306,7 @@ services: - api restart: unless-stopped + mem_limit: 128m networks: # شبکه‌ی عمومی: فقط ترافیک و سرویس‌های عمومی. postgres روی شبکه‌ی diff --git a/monitoring/docker-compose.monitoring.yml b/monitoring/docker-compose.monitoring.yml index f28e7d0..17178dc 100644 --- a/monitoring/docker-compose.monitoring.yml +++ b/monitoring/docker-compose.monitoring.yml @@ -19,7 +19,7 @@ services: # ---------------------------------------------------------------- errors -- glitchtip: - image: glitchtip/glitchtip:latest + image: glitchtip/glitchtip:latest # pinned upstream: version tags are dated, keep an eye on updates container_name: monitoring-glitchtip mem_limit: 640m environment: @@ -70,7 +70,7 @@ services: # --------------------------------------------------------------- metrics -- prometheus: - image: prom/prometheus:latest + image: prom/prometheus:v3.4.1 container_name: monitoring-prometheus mem_limit: 512m command: @@ -85,7 +85,7 @@ services: restart: unless-stopped grafana: - image: grafana/grafana:latest + image: grafana/grafana:11.6.3 container_name: monitoring-grafana mem_limit: 256m environment: @@ -105,7 +105,7 @@ services: # ------------------------------------------------------------------ logs -- loki: - image: grafana/loki:latest + image: grafana/loki:3.5.0 container_name: monitoring-loki mem_limit: 384m command: -config.file=/etc/loki/loki-config.yml @@ -117,7 +117,7 @@ services: restart: unless-stopped promtail: - image: grafana/promtail:latest + image: grafana/promtail:3.5.0 container_name: monitoring-promtail mem_limit: 64m command: -config.file=/etc/promtail/promtail-config.yml @@ -131,7 +131,7 @@ services: # -------------------------------------------------------------- exporters -- node-exporter: - image: prom/node-exporter:latest + image: prom/node-exporter:v1.9.1 container_name: monitoring-node-exporter mem_limit: 32m command: @@ -144,7 +144,7 @@ services: restart: unless-stopped postgres-exporter: - image: prometheuscommunity/postgres-postgres-exporter:latest + image: prometheuscommunity/postgres-exporter:v0.17.1 container_name: monitoring-postgres-exporter mem_limit: 48m environment: @@ -156,7 +156,7 @@ services: restart: unless-stopped cadvisor: - image: gcr.io/cadvisor/cadvisor:latest + image: gcr.io/cadvisor/cadvisor:v0.49.1 container_name: monitoring-cadvisor mem_limit: 128m privileged: true