docs: «нет заголовка Authorization или схема не Bearer» — не попытка входа (ревью R-7, #9)

Формулировка уточнена в докстрингах `services/session_audit.py` и
`api/dependencies/auth.py`, README «Безопасность dev-режима» и CLAUDE.md; тест
событий входа дополнен запросом со схемой Basic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-05 13:48:52 +04:00
parent e8d14edfbb
commit 7b28554817
5 changed files with 14 additions and 7 deletions

View File

@ -75,7 +75,8 @@
- **Аутентификация** — `core/auth.py` (проверка JWT), `src/api/dependencies/auth.py` (`current_user`, `require(code)`);
dev-эмитент (`POST /api/v1/dev/token`) существует только при `AUTH_DEV_ISSUER=true` **и** `DEBUG=true`.
Аудит входа — `services/session_audit.py` (`app.state.session_audit`): `LOGIN` (dev-токен; первый запрос после старта),
`LOGIN_FAILED` (`AuthenticationFailedError` 401/403 → обработчик пишет в отдельной транзакции `session_transaction`).
`LOGIN_FAILED` (`AuthenticationFailedError` 401/403 → обработчик пишет в отдельной транзакции `session_transaction`);
нет заголовка `Authorization` или схема не `Bearer`, 403 «недостаточно прав» — не события входа.
- **Seed** — `python -m scripts.seed [--dev]` (`make seed|seed-dev`), идемпотентен (матрица `role_permission` — целиком только в
пустую таблицу, в заполненную — лишь пары впервые созданных ролей/прав); `--dev` — только для разработки (CLI отказывает с кодом 2 при `DEBUG=false`).
**Сверка ORM↔DBML** — `scripts/check_dbml_vs_orm.py` (`make dbml-check`).

View File

@ -604,7 +604,8 @@ Dockerfile только для локального compose
- `LOGIN_FAILED` — 401 (токен не принят: `user_id` пуст, в `description` причина, сам токен не пишется) и 403
аутентификации (пользователь не зарегистрирован, отключён или неоднозначен: email в `description`, `user_id` —
если пользователь найден). Запись делается в отдельной транзакции и остаётся, хотя транзакция запроса откатилась.
- Не события входа: запрос без заголовка `Authorization` и 403 «недостаточно прав». Ошибка записи аудита входа
- Не события входа: запрос без заголовка `Authorization` или со схемой не `Bearer` (401 «требуется заголовок…») и
403 «недостаточно прав». Ошибка записи аудита входа
ответ не меняет (только лог). Выход (`LOGOUT`) не пишется: сессий на сервере нет.
- Поля, которые присылает клиент, обрезаются: `user_agent` — до 512 символов, `description` — до 1000.
- **`audit_event.ip_address` достоверен только за доверенным прокси.** `X-Forwarded-For` принимается лишь от адресов

View File

@ -10,7 +10,8 @@
Аудит входа (OQ-64, `services/session_audit.py`): отказ токена и 403 аутентификации — `AuthenticationFailedError`,
обработчик пишет LOGIN_FAILED в отдельной транзакции; первый успешный `current_user` пользователя после старта процесса
пишет LOGIN. Нет заголовка `Authorization` и 403 «недостаточно прав» — не события входа.
пишет LOGIN. Нет заголовка `Authorization` или схема не `Bearer` (`parse_bearer`) и 403 «недостаточно прав» — не
события входа.
"""
import uuid

View File

@ -6,7 +6,8 @@
и сбрасывается при рестарте, у каждого экземпляра приложения — своё);
- LOGIN_FAILED — 401 (токен не принят: `user_id` пуст, в `description` причина без самого токена) и 403 аутентификации
(пользователь не найден, отключён, неоднозначен: email в `description`, `user_id` — если пользователь найден).
Запрос без заголовка `Authorization` попыткой входа не считается; 403 «недостаточно прав» — авторизация, не вход.
Запрос без заголовка `Authorization` или со схемой не `Bearer` попыткой входа не считается (токен не предъявлен);
403 «недостаточно прав» — авторизация, не вход.
Раздел `SESSION`, `object_type = "app_user"`, `object_id = user_id`; IP и user-agent — из запроса. Поля, которые
присылает клиент, обрезаются: `user_agent` — до 512 символов, `description` — до 1000.

View File

@ -2,8 +2,9 @@
LOGIN — успешная выдача dev-токена и первый успешный запрос пользователя с токеном после старта процесса;
LOGIN_FAILED — 401 (токен не принят) и 403 аутентификации (пользователь не найден, отключён, неоднозначен).
Запрос без заголовка `Authorization` и 403 «недостаточно прав» входом не считаются. LOGIN_FAILED пишется в отдельной
транзакции и переживает откат транзакции запроса — проверяется на `committed_app` (сессия запроса как в проде).
Запрос без заголовка `Authorization` или со схемой не `Bearer` и 403 «недостаточно прав» входом не считаются.
LOGIN_FAILED пишется в отдельной транзакции и переживает откат транзакции запроса — проверяется на `committed_app`
(сессия запроса как в проде).
"""
import asyncio
@ -170,12 +171,14 @@ async def test_disabled_user_records_login_failed_with_user_id(app: FastAPI, see
async def test_missing_header_and_missing_permission_are_not_login_events(
committed_app: FastAPI, committed_sessionmaker: async_sessionmaker[AsyncSession]
) -> None:
"""Без заголовка — не попытка входа; 403 «недостаточно прав» — авторизация, не аутентификация.
"""Без заголовка или со схемой не Bearer — не попытка входа; 403 «недостаточно прав» — авторизация, не вход.
LOGIN первого запроса expert записан, хотя сам запрос (403 по праву) откатил свою транзакцию.
"""
async with _client(committed_app) as client:
assert (await client.get("/api/v1/me")).status_code == HTTP_UNAUTHORIZED
basic = {"Authorization": "Basic dXNlcjpwYXNz"}
assert (await client.get("/api/v1/me", headers=basic)).status_code == HTTP_UNAUTHORIZED
expert = _bearer(_token(EXPERT))
assert (await client.get("/api/v1/admin/users", headers=expert)).status_code == HTTP_FORBIDDEN