Files
ayurishchevandClaude Opus 5.5 13e17fbb47 Задачи 011-024: доработки по ревью кодовой базы и исправление находок
Ревью кодовой базы (docs/reviews/2026-09-26-codebase-review.md) и планы по каждой находке:
011 IP уникален в VRF и хранится в самом узком префиксе (addresses.vrf_id, составной FK
    с каскадом при переносе VRF, миграция 0007 с остановкой на дублях).
012 Ограничение попыток входа (login_attempts, 429 + Retry-After), выравнивание времени
    ответа, журнал без вытеснения анонимными событиями (миграция 0006).
013 Границы пагинации: отрицательные/чрезмерные limit/offset дают 422 вместо 500.
014 Экран адресов: страница свободных адресов арифметикой, пагинация в SQL.
015 Запрет адреса сети/broadcast, загрузка не выше 100 %.
016 Роль по умолчанию — viewer.
017 Проверка JWT_SECRET/ADMIN_PASSWORD при старте.
018 null в PATCH очищает текстовые поля; нейтральный текст конфликта БД.
019 Пакетная загрузка в списках вместо N+1.
020 Автоназначение адреса вне вложенных префиксов, с блокировкой префикса.
021 Advisory-lock при снятии прав администратора, уникальный lower(username) (миграция 0008).
022 Контейнер не от root, healthcheck, блокировка миграций, requirements.lock.
023 Экранирование LIKE, журнал отказов очистки, заголовки безопасности, учёт force-удаления,
    отзыв токенов при смене пароля (claim pv, миграция 0005).
024 Исправление находок ревью 011-023 (docs/reviews/2026-09-26-changes-011-023-review.md):
    сериализация попыток входа, запрет переноса адресов в адрес сети/broadcast, журнал входов,
    запрет смены своего пароля через PATCH, валидация PATCH устройства, обновлён тест токенов.

Тесты: 14 passed. Документация: README.md, docs/changes/011-024, docs/reviews.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:33:50 +03:00

36 lines
4.2 KiB
Markdown

# Ограничение попыток входа и защита журнала от вытеснения (изменение 012)
Находка ревью № 2, серьёзность — высокая.
## Context
`POST /auth/login` не ограничен: возможен перебор паролей без авторизации. Каждая неудача пишет `session.failed` в журнал, а ротация по количеству
(`max_entries`, 100 000) удаляет самые старые записи — анонимный клиент может вытеснить историю изменений. Дополнительно: при несуществующем логине argon2 не
вызывается (разница во времени ответа раскрывает существование логина), у пароля нет ограничения длины.
## Решение
1. **Учёт попыток** — таблица `login_attempts (id, ts, client_ip INET, username)` (новая миграция), по образцу `ClearAttempt`; индексы по `(client_ip, ts)` и `(lower(username), ts)`.
2. **Лимиты** (константы в `app/api/v1/auth.py`, при необходимости — в `Settings`):
- по логину: 5 неудач за 10 минут → блокировка входа для этого логина на 10 минут;
- по IP: 20 неудач за 10 минут → блокировка IP на 10 минут.
Ответ при блокировке — 429 `{"message", "retry_after_seconds"}` + заголовок `Retry-After`; верный пароль во время блокировки тоже отклоняется. Успешный вход очищает счётчик логина.
3. **Время ответа:** для несуществующего логина — проверка по фиктивному argon2-хэшу (вычисляется один раз при старте). `LoginIn`: `username` ≤ 100, `password` ≤ 128 символов.
4. **Журнал без вытеснения:**
- `session.failed` пишется только для первой неудачи логина/IP в окне; при срабатывании блокировки — одна запись `session.locked` (`diff`: логин, IP, число попыток, до какого времени);
- `rotate()` при превышении `max_entries` сначала удаляет самые старые `session.failed`, затем остальное — события изменений данных уходят последними.
5. **Очистка** `login_attempts` старше суток — в том же часовом цикле ротации.
6. **UI:** на экране входа — текст 429 с временем до разблокировки (уже выводится `err.message`).
## Файлы
`alembic/versions/<next>_login_attempts.py`, `app/models.py`, `app/api/v1/auth.py`, `app/schemas.py`, `app/rotation.py`, `web/app.js` (при необходимости), `tests/test_journal.py` или новый `tests/test_auth.py`, `README.md`.
## Тест (один сценарий)
6 неверных паролей для временного пользователя → на 6-й 429 с `retry_after_seconds`; верный пароль тоже 429; в журнале одна `session.failed` и одна `session.locked`.
Очистка блокировки в тесте — прямым SQL (фикстура `db`).
## Проверка
`pytest -q`; вручную: серия неверных входов в UI → сообщение о блокировке; через 10 минут вход работает.
## Риски
Блокировка по логину позволяет «запереть» чужую учётную запись перебором — поэтому окно короткое (10 минут) и событие видно в журнале.
Если стенд за reverse-proxy, без `TRUSTED_PROXIES` все клиенты делят один IP — лимит по IP заденет всех; отметить в README.