All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m38s
Deploy Trade-In / build-backend (push) Successful in 58s
Deploy Trade-In / deploy (push) Successful in 1m6s
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 (valid session 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
|