randomayzer/agents/antigravity/done/TASK-2026-08-21-02-rate-limit-client-identity.md

77 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Task 02: Client Identity for Rate Limiting Report
**Date:** 2026-08-21
**Base Commit SHA:** `92f6d1922500791ef221cc11ed63f606afc01b53`
**Status:** IMPLEMENTED / PASS
**Assigned Agent:** Antigravity (Implementation Orchestrator)
---
## 1. Executive Summary
Устранена проблема совместного использования одного общего bucket (`'direct-client'`) в механизме rate limiting при неизвестном IP (`req.ip` пуст и `TRUST_PROXY !== 'true'`), из-за которой в дефолтной конфигурации self-hosted `next start` один пользователь мог заблокировать всех остальных организаторов (выдав `429 Too Many Requests`).
Реализована модель **User-Scoped & Role-Isolated Rate Limiting**:
1. Для аутентифицированных маршрутов (`/api/giveaways*`) ключ лимита формируется строго на основе доверенного серверного идентификатора организатора `sessionUser.id`:
- `draw-execute:${sessionUser.id}:${id}`
- `snapshot-lock:${sessionUser.id}:${id}`
- `participants-import:${sessionUser.id}:${id}`
- `participants-get:${sessionUser.id}`
- `giveaway-get:${sessionUser.id}`
- `giveaways-list:${sessionUser.id}`
- `giveaway-create:${sessionUser.id}`
2. **Порядок вызовов**: аутентификация и проверка владения (`requireAuthenticatedUser` / `requireGiveawayOwner`) теперь выполняются **до** вызова рейт-лимитера. Неаутентифицированные запросы сразу отклоняются со статусом `401 Unauthorized` и не имеют возможности исчерпать или затронуть квоту организатора.
3. Для гибридного эндпоинта `POST /api/posts/preview`: при наличии активной сессии используется ключ `post-preview:user:${sessionUser.id}`, а для анонимных пользователей — `post-preview:anon:${clientIp}`. Анонимный спам не блокирует авторизованных пользователей.
4. В `src/lib/client-ip.ts` добавлен однократный `[SECURITY CONFIGURATION WARNING]` в production, если `TRUST_PROXY !== 'true'` и `req.ip` пуст. Поведение задокументировано в `docs/PRODUCTION_GUARDS.md`.
---
## 2. Modified Files
| File | Type | Description |
|------|------|-------------|
| `src/lib/client-ip.ts` | Security | Добавлено предупреждение в production при отсутствии `TRUST_PROXY` и пустом `req.ip`. |
| `src/app/api/giveaways/[id]/draw/route.ts` | API Route | Перенесён вызов `requireGiveawayOwner` перед лимитером; ключ лимита `draw-execute:${sessionUser.id}:${id}`. |
| `src/app/api/giveaways/[id]/snapshot/route.ts` | API Route | Перенесён вызов `requireGiveawayOwner` перед лимитером; ключ лимита `snapshot-lock:${sessionUser.id}:${id}`. |
| `src/app/api/giveaways/[id]/participants/route.ts` | API Route | Аутентификация перед лимитером; ключи `participants-get:${sessionUser.id}` и `participants-import:${sessionUser.id}:${id}`. |
| `src/app/api/giveaways/[id]/route.ts` | API Route | Аутентификация перед лимитером; ключ `giveaway-get:${sessionUser.id}`. |
| `src/app/api/giveaways/route.ts` | API Route | Аутентификация перед лимитером; ключи `giveaways-list:${sessionUser.id}` и `giveaway-create:${sessionUser.id}`. |
| `src/app/api/posts/preview/route.ts` | API Route | Разделение ключей на `post-preview:user:${sessionUser.id}` и `post-preview:anon:${clientIp}`. |
| `docs/PRODUCTION_GUARDS.md` | Docs | Обновлен раздел 2 (Client Identity Scoping Architecture, Trust Proxy Modes). |
| `tests/rate-limit-identity.test.ts` | Tests (NEW) | Набор тестов изоляции лимитов организаторов, анонимных пользователей и защиты от обхода (5 тестов). |
---
## 3. Verification Evidence & Test Gate
Фактически выполненные команды:
```text
npx prisma generate -> EXIT 0 (Prisma Client v5.22.0)
npx tsc --noEmit -> EXIT 0 (Clean TypeScript check, 0 errors)
npm test -> EXIT 0 (51 test files, 300 tests passed, 0 failed)
npm run lint -> EXIT 0 (Next.js ESLint passed clean)
npm run build -> EXIT 0 (Next.js production build compiled successfully)
```
### Summary of New Tests (`tests/rate-limit-identity.test.ts`):
- `two distinct organizers with empty req.ip have independent draw rate limits`**PASS**
- `organizer listing rate limit is scoped by sessionUser.id`**PASS**
- `exhausting anonymous rate limit on post preview does not block authenticated organizers`**PASS**
- `unauthenticated request fails with 401 without affecting organizer rate limit bucket`**PASS**
- `uses validated client IP for anonymous endpoints when TRUST_PROXY=true`**PASS**
---
## 4. Core Invariants & Security
- **Randomizer / Audit Proof Invariants:** Алгоритмы `HMAC_SHA256_FY_V1`, `executeDeterministicDrawV1`, `verifyDrawResult` сохранены без изменений.
- **Fail-Closed Authorization:** Любой неаутентифицированный или cross-tenant запрос отклоняется `401`/`403` до изменения счётчиков лимитера.
- **Proxy Header Integrity:** При `TRUST_PROXY !== 'true'` клиентские заголовки `X-Forwarded-For` по-прежнему строго игнорируются во избежание IP-spoofing.
---
## 5. UNVERIFIED Assertions & Tech Debt
1. **UNVERIFIED: Distributed Edge / Redis Rate Limiter:**
- В текущей реализации лимитер остаётся in-memory (`SlidingWindowRateLimiter`). Для горизонтально масштабируемых кластеров рекомендуется подключение Redis/Valkey или edge-уровня (Cloudflare Rate Limiting).