Задача 037: двухфакторная аутентификация TOTP по выбору пользователя

Пользователь сам включает TOTP (QR, приложение-аутентификатор); 10 одноразовых кодов
восстановления; superadmin сбрасывает 2FA другому пользователю. Вход в два шага:
/auth/login → mfa_token, /auth/login/2fa — под теми же лимитами и advisory-lock, что пароль.
Секрет шифруется Fernet-ключом TOTP_ENC_KEY, защита от повторного использования кода
(totp_last_step), лимиты перебора кода при отключении. Миграция 0015, зависимости pyotp,
segno, cryptography. UI: второй шаг входа, диалоги 2FA в меню логина, бейдж и сброс
в «Пользователях»; submitDialog не закрывает окно, открытое следующим шагом цепочки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Opus 5.5 committed 2026-09-27 16:57:14 +03:00
1 parent c852f47f09
commit d3495fb077
18 files changed
+835 -56

No files matched your search

+14 -4
View File
@@ -23,12 +23,13 @@ venv/bin/python scripts/seed_demo.py # по желанию: демо-да
| `APP_BIND` | Адрес публикации порта: по умолчанию `0.0.0.0`; `127.0.0.1` — только за reverse-proxy |
| `DB_HOST_PORT` | Порт PostgreSQL на хосте, публикуется только на `127.0.0.1` |
| `TRUSTED_PROXIES` | CIDR доверенных прокси через запятую; только от них принимается `X-Forwarded-For` |
| `TOTP_ENC_KEY` | Ключ Fernet для шифрования секретов 2FA (изменение 037). Не задан — приложение стартует, но 2FA недоступна (503); некорректный ключ — отказ старта; потеря ключа означает сброс 2FA всем пользователям |
## Архитектура
| Слой | Технологии |
|---|---|
| API | FastAPI, pydantic v2, JWT (срок 8 ч), пароли в argon2, роли `superadmin`, `admin`, `viewer` с привязкой к организации |
| БД | PostgreSQL 16, SQLAlchemy 2, Alembic (миграции `0001`–`0014`), типы `CIDR`/`INET` |
| БД | PostgreSQL 16, SQLAlchemy 2, Alembic (миграции `0001`–`0015`), типы `CIDR`/`INET` |
| UI | Статический SPA (vanilla JS, ES-модуль) раздаётся приложением; шрифты IBM Plex хранятся локально, внешних зависимостей нет |
```
@@ -78,11 +79,11 @@ docs/changes/ планы и итоги доработок docs/reviews/
## API (`/api/v1`)
| Область | Эндпоинты |
|---|---|
| Вход | `POST /auth/login`, `GET /auth/me` |
| Вход | `POST /auth/login`, `POST /auth/login/2fa` (изменение 037), `GET /auth/me` |
| Справочники | `/organizations`, `/vrfs`, `/isps`, `/device-types`, `/devices` |
| Префиксы | `/prefixes`, `GET\|POST /prefixes/{id}/subnets/next` (предпросмотр и автовыделение вложенного) |
| Адреса | `GET\|POST /prefixes/{id}/addresses`, `POST /prefixes/{id}/addresses/next` (автоназначение из пула), `PATCH\|DELETE /addresses/{id}` |
| Пользователи | `/users`, `POST /users/me/password` |
| Пользователи | `/users`, `POST /users/me/password`, `POST /users/me/2fa/setup\|enable\|disable`, `POST /users/{id}/2fa/reset` (изменение 037) |
| Журнал | `GET /audit`, `/audit/summary`, `/audit/facets`, `/audit/{uid}`, `GET\|PUT /journal/settings`, `POST /journal/clear` |
| Сводка | `GET /overview` |
@@ -112,6 +113,13 @@ docs/changes/ планы и итоги доработок docs/reviews/
- 20 — на IP;
- 50 — на логин со всех IP, кроме тех, с которых пользователь успешно входил за 30 дней.
- Сверх лимита — 429 с `Retry-After`.
- Двухфакторная аутентификация TOTP — по желанию пользователя (изменение 037), не обязательна. Включается в меню логина
в шапке: пароль → QR-код (Google Authenticator, Aegis, 1Password, Bitwarden и подобные) → код подтверждения →
10 одноразовых кодов восстановления (показываются один раз). При включённой 2FA вход идёт в два шага:
`POST /auth/login` возвращает `mfa_token` вместо токена доступа, `POST /auth/login/2fa` принимает код из приложения
или код восстановления. Неверный код учитывается в тех же лимитах перебора, что и пароль. Секрет хранится в БД
зашифрованным ключом `TOTP_ENC_KEY`; коды восстановления — хэшем sha256. Суперадминистратор может сбросить 2FA
другому пользователю (`POST /users/{id}/2fa/reset`), не отключая свою.
**Журнал**
- Хранит IP клиента и метаданные запроса.
@@ -141,7 +149,8 @@ docs/changes/ планы и итоги доработок docs/reviews/
## Интерфейс
- Экраны: «Обзор», «Префиксы» (дерево по VRF), «Адреса» подсети, «Организации», «Операторы», «Устройства», «Журнал», «Пользователи» (только `superadmin`).
- В «Пользователях» видна дата и IP последнего входа (изменение 035); «—», если пользователь ещё не входил.
- Логин в шапке раскрывает меню «Сменить пароль» / «Выйти» (изменение 036).
- Логин в шапке раскрывает меню «Сменить пароль» / «Двухфакторная аутентификация» / «Выйти» (изменения 036, 037).
- В «Пользователях» у записей с включённой 2FA — бейдж «2FA»; в меню строки суперадминистратора для чужих записей — «Сбросить 2FA» (изменение 037).
- Переключатель организации — только у `superadmin`; `admin`/`viewer` работают в своей организации.
- Строка реестра кликабельна целиком. Действия над строкой — в меню «⋯».
- Групповые операции через чекбоксы (кроме «Журнала»): удаление, смена типа устройств, статус префиксов и адресов, доступ пользователей.
@@ -193,6 +202,7 @@ docker compose -p ipam_control_006 up -d --build && venv/bin/python -m pytest -q
| 034 | Публикация через Caddy: rxipam.rxmsk.ru | [план](docs/changes/034-caddy-publication/PLAN.md) · [итог](docs/changes/034-caddy-publication/SUMMARY.md) |
| 035 | Последний вход пользователя: дата и IP | [план](docs/changes/035-user-last-login/PLAN.md) · [итог](docs/changes/035-user-last-login/SUMMARY.md) |
| 036 | Меню пользователя в шапке: логин с выпадающим списком | [план](docs/changes/036-user-menu/PLAN.md) · [итог](docs/changes/036-user-menu/SUMMARY.md) |
| 037 | Двухфакторная аутентификация TOTP, по выбору пользователя | [план](docs/changes/037-totp-2fa/PLAN.md) · [итог](docs/changes/037-totp-2fa/SUMMARY.md) |
## Отчёты ревью
- [Ревью кодовой базы](docs/reviews/2026-09-26-codebase-review.md) (находки → изменения 011–023)