docs(06-05): add plan summary — D-11/D-12 trusted-proxy IP + per-account limiter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
a826738e18
commit
5c82a9840a
@@ -0,0 +1,70 @@
|
||||
---
|
||||
plan: 06-05
|
||||
phase: 06-performance-production-hardening
|
||||
status: complete
|
||||
completed_at: 2026-06-04
|
||||
commits:
|
||||
- a826738
|
||||
self_check: PASSED
|
||||
---
|
||||
|
||||
## What was done
|
||||
|
||||
### D-11 — Trusted-proxy CIDR check in `get_client_ip`
|
||||
|
||||
**Before:** `get_client_ip` unconditionally returned the first value of
|
||||
`X-Forwarded-For` if present, falling back to `request.client.host`. Any
|
||||
external caller could spoof their IP by adding an `X-Forwarded-For` header.
|
||||
|
||||
**After:** `get_client_ip` only honours `X-Forwarded-For` when the direct peer
|
||||
(`request.client.host`) is in one of the trusted proxy CIDRs:
|
||||
- `127.0.0.0/8` (loopback)
|
||||
- `172.16.0.0/12` (Docker / internal)
|
||||
- `192.168.0.0/16` (LAN)
|
||||
- `::1/128` (IPv6 loopback)
|
||||
|
||||
Untrusted peers always return their own address. The CIDR list is a module-level
|
||||
constant `_TRUSTED_PROXY_NETS` in `backend/deps/utils.py`. No new public function
|
||||
was added; only the body and docstring of `get_client_ip` changed.
|
||||
|
||||
### D-12 — Per-account rate limiter (`account_limiter`)
|
||||
|
||||
Created `backend/services/rate_limiting.py` (new file, 18 lines):
|
||||
- `_account_key(request)`: reads `request.state.current_user.id` if set,
|
||||
falls back to `request.client.host`, then `"anonymous"`.
|
||||
- `account_limiter = Limiter(key_func=_account_key)`: singleton exported for
|
||||
use in routers.
|
||||
|
||||
### Wiring
|
||||
|
||||
- `backend/api/auth.py`: removed `from slowapi.util import get_remote_address`;
|
||||
changed `limiter = Limiter(key_func=get_remote_address)` to
|
||||
`limiter = Limiter(key_func=get_client_ip)`.
|
||||
- `backend/main.py`: added `from services.rate_limiting import account_limiter`.
|
||||
- `backend/api/documents.py`: 9 endpoints decorated with
|
||||
`@account_limiter.limit("100/minute")`; each handler's first line sets
|
||||
`request.state.current_user = current_user` (A1 ordering invariant).
|
||||
- `backend/api/cloud.py`: 7 endpoints decorated identically (same pattern).
|
||||
|
||||
**Decorator counts:**
|
||||
- `documents.py`: 9 `@account_limiter.limit` decorators, 9 assignments
|
||||
- `cloud.py`: 7 `@account_limiter.limit` decorators, 7 assignments
|
||||
|
||||
### D-13 — 8 xfail tests promoted
|
||||
|
||||
All 8 stubs in `backend/tests/test_rate_limiting.py` promoted from `xfail` to
|
||||
real assertions. No `@pytest.mark.xfail` markers remain.
|
||||
|
||||
A cross-test contamination issue was also fixed: `backend/tests/conftest.py`
|
||||
gained an autouse fixture `reset_rate_limiter` that calls
|
||||
`account_limiter._storage.reset()` before and after each test, preventing
|
||||
in-memory counters from accumulating across tests and causing spurious 429s.
|
||||
|
||||
## Final pytest result
|
||||
|
||||
```
|
||||
352 passed, 5 skipped, 7 xfailed
|
||||
1 pre-existing failure (test_extract_docx — ModuleNotFoundError: No module named 'docx', unrelated to this plan)
|
||||
```
|
||||
|
||||
All 8 rate limiting tests pass (8 passed, 0 xfailed).
|
||||
Reference in New Issue
Block a user