gendesign/tradein-mvp/backend/app/api/v1/me.py
Light1YT 3651c85f9b feat(rbac): enforce role-based access via X-Authenticated-User middleware
Backend-side enforcement поверх Caddy basic_auth — defense-in-depth для
12-юзеров на gendsgn.ru (admin + kopylov + 10 пилотных слотов).

- `auth/roles.yaml` — single source of truth: 2 роли (admin, pilot)
  + 12 user→role mappings. Bind-mounted в обоих backend контейнерах.
- `app/core/auth.py` (main + tradein-mvp mirror) — YAML loader через
  `pyyaml`, `get_role`/`get_user_scope`/`is_path_allowed` с fnmatch globs.
  Custom `_glob_to_regex` чтобы `/**` ≠ `/*` (matches multi-segment paths).
- `GET /api/v1/me` (main + tradein) — фронт читает scope при login,
  фильтрует nav по `allowed_paths`/`deny_paths`.
- `rbac_guard` middleware (main + tradein):
  * любой non-public path требует `X-Authenticated-User` → иначе 401
  * юзер должен быть в roles.yaml → иначе 403 («человек без ролей
    вообще ничего не видит» — decided 2026-05-25)
  * /api/v1/admin/* — только role=admin, иначе 403
- `Caddyfile` — `header_up X-Authenticated-User {http.auth.user.id}` в
  6 reverse_proxy блоках (gendsgn.ru main + trade-in + git.gendsgn.ru).
- Tests: 20 unit + integration в обоих стэках. Покрытие:
  no-header / unknown user / pilot / admin × admin-path / non-admin path.
  `_build_test_app()` копирует middleware inline, чтобы обойти heavy
  imports (weasyprint dlopen падает на macOS dev).

Public paths (/health, /docs, /redoc, /openapi.json) пропускаются —
X-Authenticated-User там просто не приходит из Caddy.

Frontend nav-filter + 403 fullscreen — отдельный PR B.
2026-05-26 11:14:47 +05:00

46 lines
1.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 чтобы понять
кому что показывать.
"""
from __future__ import annotations
import logging
from typing import Annotated
from fastapi import APIRouter, Header, HTTPException
from app.core.auth import UserScope, get_user_scope
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/me")
async def me(
x_authenticated_user: Annotated[str | None, Header(alias="X-Authenticated-User")] = None,
) -> UserScope:
"""Return the current user's RBAC scope (role + allowed/deny paths)."""
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