All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m3s
CI Trade-In / backend-tests (pull_request) Successful in 2m40s
Дефолт не меняет ничего: IDENTITY_STORE="tradein" — это сегодняшний прод, tradein_users/tradein_sessions, соединение с БД auth не открывается вообще. Переключение делается одной переменной окружения ПОСЛЕ того, как на проде появится пароль auth_app и будут скопированы данные. Так сделано намеренно: мерж, который зависит от невыполненного ручного шага, — это мерж, который ломает прод в момент невнимательности. Ядро. app/services/identity_store.py — единственное место, знающее, в какой БД и в каких таблицах живёт реестр. Имена таблиц берутся из фиксированного словаря по значению флага, не конкатенацией с вводом. app/core/auth_db.py — ЛЕНИВЫЙ engine БД auth (core/db.py создаёт свой на импорте; такое же для auth роняло бы старт без DSN). Одно понятие состояния доступа вместо двух. В tradein_users состояние — булев is_active, в auth.users — access_state из трёх значений. Конверсия живёт в одной функции to_access_state(): True→active, False→disabled, а неизвестная строка, NULL или чужой тип → disabled с WARNING. Fail-closed выбран сознательно: если следующая миграция добавит четвёртое состояние, оно по умолчанию НЕ будет пускать. Проверка доступа — свойство can_sign_in, а не сравнение со строкой. Логин в режиме auth. Пароль проверяется ВСЕГДА и ДО ветвления по состоянию — иначе появляется timing-oracle и перечисление логинов. Верный пароль + trial_expired → 403 с машиночитаемым code="access_expired", сессия НЕ создаётся. Верный пароль + disabled → тот же generic 401, что и при неверном пароле. Резолв уже выданной сессии пропускает только active — блокировка обрывает сессию немедленно, а не по истечении sliding-refresh. Старт падает явно, если IDENTITY_STORE=auth, а DSN не задан. Без этого ошибка конфигурации не похожа на аварию: продуктовая БД жива, приложение работает, а rbac_guard ловит исключение резолва вместе с любым другим сбоем и падает в legacy trusted-header ветку — то есть сутками раздаёт права из roles.yaml мимо реестра, включая аккаунты с disabled. Форма входа понимает новый код ответа. Ветвление по detail.code, а не по тексту: текст бэк вправе менять, код — нет. Гранты соблюдены, а не обойдены: auth_app не имеет UPDATE на role/manager_id и не имеет DELETE на users (миграция 004, column-level). Тесты: 2996 passed (+59). Единственный красный — test_search_cache_hit — предсуществующий: проверен контрольным полным прогоном на чистом main (2937 passed, тот же красный).
93 lines
4 KiB
Python
93 lines
4 KiB
Python
"""GET /me — отдаёт текущего пользователя и его RBAC-scope.
|
||
|
||
MIRROR of main backend's app/api/v1/me.py — kept in sync manually.
|
||
Mounted at /api/v1/me; через Caddy `uri strip_prefix /trade-in` это становится
|
||
`/trade-in/api/v1/me` снаружи.
|
||
|
||
Caddy basic_auth пропускает `X-Authenticated-User: <username>` через
|
||
`header_up` в каждом reverse_proxy. Frontend дёргает /me чтобы понять
|
||
кому что показывать.
|
||
|
||
#2552: session-first. Валидная DB-session cookie (см. app.services.auth_session)
|
||
отдаёт scope из реестра людей (role/display_name/org/email) БЕЗ похода в
|
||
roles.yaml. Без cookie (или невалидная/истёкшая) — legacy X-Authenticated-User
|
||
путь, БЕЗ ИЗМЕНЕНИЙ (regression недопустим — существующие тесты держат его
|
||
бит-в-бит).
|
||
|
||
Сессия БД берётся у `identity_store.get_identity_db` (реестр), а не у
|
||
`app.core.db.get_db` (продуктовая БД): при `IDENTITY_STORE=auth` люди и сессии
|
||
живут в другой БД. В дефолтном режиме это ТОТ ЖЕ объект `Session`, что отдал бы
|
||
`get_db`, — поведение прода не меняется.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Annotated, Any
|
||
|
||
from fastapi import APIRouter, Depends, Header, HTTPException, Request
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.auth import UserScope, get_user_scope
|
||
from app.core.config import settings
|
||
from app.services.auth_session import get_db_role_scope, get_session_user
|
||
from app.services.identity_store import get_identity_db
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
@router.get("/me")
|
||
async def me(
|
||
request: Request,
|
||
db: Annotated[Session, Depends(get_identity_db)],
|
||
x_authenticated_user: Annotated[str | None, Header(alias="X-Authenticated-User")] = None,
|
||
) -> UserScope | dict[str, Any]:
|
||
"""Return the current user's RBAC scope (role + allowed/deny paths).
|
||
|
||
Return type is a union (не только `UserScope`) — `UserScope.role` — это
|
||
`Literal["admin","pilot","analyst","expired"]` (legacy roles.yaml names),
|
||
а DB-роли (реестр: tradein_users.role / auth.users.role) —
|
||
`"admin"/"manager"/"employee"`. FastAPI
|
||
строит response-схему из return-аннотации; жёсткий `UserScope` завернул бы
|
||
"employee"/"manager" в ResponseValidationError. Итоговая JSON-форма
|
||
ОДИНАКОВАЯ (те же 8 ключей) для обеих веток.
|
||
"""
|
||
token = request.cookies.get(settings.session_cookie_name)
|
||
if token:
|
||
try:
|
||
session_user = get_session_user(db, token)
|
||
except Exception:
|
||
logger.exception("me: session lookup failed")
|
||
session_user = None
|
||
if session_user is not None:
|
||
role = session_user["role"]
|
||
allowed_paths, deny_paths = get_db_role_scope(role)
|
||
return {
|
||
"username": session_user["username"],
|
||
"role": role,
|
||
"allowed_paths": allowed_paths,
|
||
"deny_paths": deny_paths,
|
||
"brand": None,
|
||
"display_name": session_user["display_name"],
|
||
"org": session_user["org_name"],
|
||
"email": session_user["email"],
|
||
}
|
||
|
||
if not x_authenticated_user:
|
||
raise HTTPException(
|
||
status_code=401,
|
||
detail="no authenticated user (Caddy basic_auth required)",
|
||
)
|
||
try:
|
||
return get_user_scope(x_authenticated_user)
|
||
except KeyError:
|
||
logger.warning(
|
||
"user %r authenticated via Caddy but missing from roles.yaml",
|
||
x_authenticated_user,
|
||
)
|
||
raise HTTPException(
|
||
status_code=403,
|
||
detail="user not in roles config",
|
||
) from None
|