Задача 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:
1 parent
c852f47f09
commit
d3495fb077
18 files changed
+835
-56
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user