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.
46 lines
1.5 KiB
Python
46 lines
1.5 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 чтобы понять
|
||
кому что показывать.
|
||
"""
|
||
|
||
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
|