Files

28 lines
3.4 KiB
Markdown
Raw Permalink Normal View History

# Итог: сериализация попыток входа по IP (изменение 027)
Выполнено вместе с изменением 026 (один файл `app/api/v1/auth.py`, один проход); детали трёх областей лимита — в SUMMARY 026.
## Что сделано
- `app/api/v1/auth.py`, `login`: после advisory-lock по логину (`pg_advisory_xact_lock(LOGIN_LOCK_NS, hashtext(name))`, изменение 024) и до `_retry_after` — вторая блокировка по IP,
если `ip` известен: `pg_advisory_xact_lock(IP_LOCK_NS, hashtext(ip))`. Порядок всегда «логин, затем IP» — исключает взаимную блокировку.
- Использована двухаргументная форма advisory-lock с раздельными пространствами ключей (`LOGIN_LOCK_NS = 7031`, `IP_LOCK_NS = 7032`), чтобы `hashtext(логин)` и `hashtext(ip)`
гарантированно не пересекались (не зависит от совпадения хэшей, как было бы при одноаргументной форме с общим пространством).
- Комментарий в коде — плата за сериализацию: попытки с одного IP (в том числе за NAT) обрабатываются по одной, время ответа при массовом переборе растёт на время проверки argon2.
- `README.md`, абзац «Вход»: добавлено предложение о второй сериализации и её цене.
## Отклонения от плана
Нет.
## Как проверено
1. `venv/bin/python -c 'import app.main'` — без ошибок.
2. Стенд пересобран и здоров (см. SUMMARY 026 — общая пересборка для 026+027).
3. Сценарий из плана: 15 неудачных попыток по IP (докер-шлюз) вставлены напрямую в `login_attempts` (порог `MAX_PER_IP = 20`, то есть до блокировки осталось 5); затем 16 параллельных
(через `ThreadPoolExecutor`, 16 потоков) неверных входов на 16 разных **несуществующих** логинов с этого же IP:
- ответы: 4×401, 12×429;
- новых строк в `login_attempts` для этого IP (кроме затравки) — **5** (запрос, доведший счётчик до 20, тоже проверяет пароль и добавляет строку, но получает 429, а не 401 —
поэтому 401 на один меньше числа добавленных строк, само число строк не превышает лимит). Соответствует условию плана «добавилось ≤ 5 записей». **PASS.**
4. `login_attempts` очищена после проверки.
## Не проверено
- Поведение при коллизии `hashtext` между конкретным логином и конкретным IP в старой (одноаргументной) схеме — не воспроизводилось намеренно, замена на двухаргументную форму
устраняет саму возможность, отдельно не тестировалось.