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)`); - **Аутентификация** — `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`. dev-эмитент (`POST /api/v1/dev/token`) существует только при `AUTH_DEV_ISSUER=true` **и** `DEBUG=true`.
Аудит входа — `services/session_audit.py` (`app.state.session_audit`): `LOGIN` (dev-токен; первый запрос после старта), Аудит входа — `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` — целиком только в - **Seed** — `python -m scripts.seed [--dev]` (`make seed|seed-dev`), идемпотентен (матрица `role_permission` — целиком только в
пустую таблицу, в заполненную — лишь пары впервые созданных ролей/прав); `--dev` — только для разработки (CLI отказывает с кодом 2 при `DEBUG=false`). пустую таблицу, в заполненную — лишь пары впервые созданных ролей/прав); `--dev` — только для разработки (CLI отказывает с кодом 2 при `DEBUG=false`).
**Сверка ORM↔DBML** — `scripts/check_dbml_vs_orm.py` (`make dbml-check`). **Сверка 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 - `LOGIN_FAILED` — 401 (токен не принят: `user_id` пуст, в `description` причина, сам токен не пишется) и 403
аутентификации (пользователь не зарегистрирован, отключён или неоднозначен: email в `description`, `user_id` — аутентификации (пользователь не зарегистрирован, отключён или неоднозначен: email в `description`, `user_id` —
если пользователь найден). Запись делается в отдельной транзакции и остаётся, хотя транзакция запроса откатилась. если пользователь найден). Запись делается в отдельной транзакции и остаётся, хотя транзакция запроса откатилась.
- Не события входа: запрос без заголовка `Authorization` и 403 «недостаточно прав». Ошибка записи аудита входа - Не события входа: запрос без заголовка `Authorization` или со схемой не `Bearer` (401 «требуется заголовок…») и
403 «недостаточно прав». Ошибка записи аудита входа
ответ не меняет (только лог). Выход (`LOGOUT`) не пишется: сессий на сервере нет. ответ не меняет (только лог). Выход (`LOGOUT`) не пишется: сессий на сервере нет.
- Поля, которые присылает клиент, обрезаются: `user_agent` — до 512 символов, `description` — до 1000. - Поля, которые присылает клиент, обрезаются: `user_agent` — до 512 символов, `description` — до 1000.
- **`audit_event.ip_address` достоверен только за доверенным прокси.** `X-Forwarded-For` принимается лишь от адресов - **`audit_event.ip_address` достоверен только за доверенным прокси.** `X-Forwarded-For` принимается лишь от адресов

View File

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

View File

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

View File

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