From 7b28554817bda4f79fc70b8ee2861f87abf146da Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 5 Oct 2026 13:48:52 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20=C2=AB=D0=BD=D0=B5=D1=82=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=B0=20Authorization=20?= =?UTF-8?q?=D0=B8=D0=BB=D0=B8=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B5=20Bearer=C2=BB=20=E2=80=94=20=D0=BD=D0=B5=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BF=D1=8B=D1=82=D0=BA=D0=B0=20=D0=B2=D1=85=D0=BE=D0=B4=D0=B0?= =?UTF-8?q?=20(=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20R-7,=20#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Формулировка уточнена в докстрингах `services/session_audit.py` и `api/dependencies/auth.py`, README «Безопасность dev-режима» и CLAUDE.md; тест событий входа дополнен запросом со схемой Basic. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- CLAUDE.md | 3 ++- README.md | 3 ++- api/src/api/dependencies/auth.py | 3 ++- api/src/services/session_audit.py | 3 ++- api/tests/test_login_audit.py | 9 ++++++--- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3dbc7f0..eb239a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`). diff --git a/README.md b/README.md index 6c8ba81..afcb388 100644 --- a/README.md +++ b/README.md @@ -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` принимается лишь от адресов diff --git a/api/src/api/dependencies/auth.py b/api/src/api/dependencies/auth.py index 9ad2839..e988fc8 100644 --- a/api/src/api/dependencies/auth.py +++ b/api/src/api/dependencies/auth.py @@ -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 diff --git a/api/src/services/session_audit.py b/api/src/services/session_audit.py index 16264e5..f623661 100644 --- a/api/src/services/session_audit.py +++ b/api/src/services/session_audit.py @@ -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. diff --git a/api/tests/test_login_audit.py b/api/tests/test_login_audit.py index da3b984..e5b17e3 100644 --- a/api/tests/test_login_audit.py +++ b/api/tests/test_login_audit.py @@ -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