Merge pull request 'feat(tradein/caddy): снять basic_auth с /trade-in — своя авторизация (#2558)' (#2569) from feat/tradein-caddy-cutover into main
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy / build-frontend (push) Successful in 35s
Deploy / build-backend (push) Successful in 36s
Deploy / build-worker (push) Successful in 36s
Deploy / deploy (push) Successful in 1m8s

This commit is contained in:
lekss361 2026-07-30 21:52:00 +00:00
commit f6b68b6eca

View file

@ -11,6 +11,13 @@
# Users managed via caddy/users.caddy.snippet (git history = audit trail).
# Public exclusions: /health (liveness probe), /preview/* (static mockups).
#
# #2558: с 2026-07 basic_auth гейтит ТОЛЬКО Site Finder (`/`, `/api/*`,
# `/analytics` и т.д.). `/trade-in/*` (+ `/sale-share` redirect) вынесены ВЫШЕ
# import'ау trade-in своя авторизация (форма входа + opaque session-cookie,
# см. #2552) поверх RBAC (`tradein-mvp/backend/app/core/rbac.py`). Site Finder
# всё ещё легаси-пилотный basic_auth (roles.yaml dual-mode остаётся живым для
# него — НЕ трогать caddy/users.caddy.snippet).
#
# IMPORTANT: route { } block is required to preserve directive order.
# Without route { }, Caddy executes directives in hard-coded default order
# (basic_auth runs before handle), making /health and /preview/* exclusions
@ -70,26 +77,56 @@ gendsgn.ru {
# Оба ДО auth-import, иначе ассеты страницы уходят в @tradein (под auth) → 401 → без CSS.
@uipreview path /trade-in/ui-preview/* /trade-in/_next/static/*
handle @uipreview {
reverse_proxy tradein-frontend:3000
reverse_proxy tradein-frontend:3000 {
# #2558 review: тот же периметр-scrub, что и у /trade-in/api/* и
# @tradein ниже — этот блок тоже теперь ДО basic_auth, клиент
# мог бы прислать свой X-Authenticated-User. Сейчас инертно
# (страница статична, у tradein-frontend нет секрета для
# X-Internal-Auth-Secret), но убираем ради единообразия периметра,
# а не полагаясь на то, что downstream ничего не делает с заголовком.
header_up -X-Authenticated-User
}
}
# Auth gate (applies to all routes below within this route block).
import caddy/users.caddy.snippet
# Trade-In MVP subproject (tradein-mvp/) — gendesign-tradein docker stack,
# подключен через gendesign_shared network. Routes ДО универсального handle
# потому что Caddy матчит handle-блоки сверху вниз.
# #2558: Trade-In MVP subproject (tradein-mvp/) — gendesign-tradein docker
# stack, подключен через gendesign_shared network. Секция ЦЕЛИКОМ ДО
# `import caddy/users.caddy.snippet` ниже — /trade-in имеет собственную
# авторизацию (форма входа + opaque session-cookie, #2552; RBAC-проверка
# роли внутри tradein-backend, `app/core/rbac.py`), Site Finder basic_auth
# ей больше не нужен и не должен применяться (short-circuit сверху вниз,
# как /health и /preview/* выше).
#
# X-Authenticated-User — ЯВНОЕ УДАЛЕНИЕ (`header_up -X-Authenticated-User`),
# НЕ `header_up X-Authenticated-User {http.auth.user.id}`. Причина: этот
# блок больше не идёт ПОСЛЕ basic_auth, поэтому `{http.auth.user.id}`
# никогда не резолвится авторизованным юзером на этом пути.
# Проверено эмпирически (echo-стенд на образе caddy:2, `caddy adapt`):
# старая Set-форма (`header_up X-Authenticated-User {http.auth.user.id}`)
# НЕ пропустила бы клиентский заголовок насквозь и НЕ оставила бы поле
# пустым — Caddy подставляет НЕРАЗРЕШЁННЫЙ плейсхолдер как ЛИТЕРАЛЬНУЮ
# строку (`ReplaceKnown`), т.е. upstream получил бы буквально
# `X-Authenticated-User: {http.auth.user.id}`. Для backend (auth_mode=
# "dual", `app/core/config.py`) это НЕ подмена личности — legacy path
# (`rbac.py:186`) сделал бы `get_role("{http.auth.user.id}")`, юзер не
# найден в roles.yaml → 403 для всех. Т.е. старая форма была бы не
# security-дырой, а fail-closed-but-сломанной (все trade-in запросы без
# session-cookie получали бы 403 вместо ожидаемого 401/редиректа на логин).
# `-Field` остаётся правильным выбором не потому что Set был бы дырой, а
# потому что это ЕДИНСТВЕННАЯ форма с явно задокументированной семантикой
# "удалить заголовок" (Caddyfile reverse_proxy directive: `-<field>` =
# delete) — корректное поведение не должно зависеть от того, как именно
# Caddy трактует нерезолвленный/пустой плейсхолдер в Set-операции.
# X-Internal-Auth-Secret НЕ трогаем — #2213-секрет всегда перезаписывается
# из env (Set-операция с непустым значением, никак не связана с auth-гейтом
# basic_auth), это единственное, что теперь отсекает подделку заголовков
# изнутри gendesign_shared network для legacy dual-mode пути.
handle /trade-in/api/* {
# `handle_path /trade-in/api/*` стрипал бы целиком /trade-in/api;
# FastAPI router замаунтен на /api/v1/trade-in/* — нужен strip только
# префикса basePath /trade-in (Next.js basePath leak).
uri strip_prefix /trade-in
reverse_proxy tradein-backend:8000 {
header_up X-Authenticated-User {http.auth.user.id}
# #2213 defense-in-depth: общий секрет Caddy↔tradein-backend. header_up
# с value ПЕРЕЗАПИСЫВАЕТ (стирает) любой клиентский X-Internal-Auth-Secret —
# тот же механизм, что защищает X-Authenticated-User выше. Пусто пока
# TRADEIN_INTERNAL_AUTH_SECRET не задан в .env (fail-open, backend не проверяет).
header_up -X-Authenticated-User
header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
}
}
@ -98,6 +135,16 @@ gendsgn.ru {
# Next basePath=/trade-in → редиректим на канонический /trade-in/sale-share
# (тот же tradein-frontend контейнер; query-string сохраняется). True vanity-URL
# в адресной строке требует отдельного Next-app с basePath=/sale-share.
# #2558: перенесён ВЫШЕ auth-import вместе с trade-in — редирект ведёт на
# /trade-in/sale-share, для которого теперь нет Caddy basic_auth (как и
# для остального /trade-in). Это НЕ делает страницу публичной: она всё
# ещё за собственной авторизацией trade-in — `RouteGuard` во фронте
# (`app/layout.tsx`, «Доступ: pilot + admin») и сессия для
# `/api/v1/buildings/sale-share*` на бэке; без валидной сессии юзер
# получит редирект на /login, а не контент. Смысл переноса — не открыть
# страницу всем, а убрать несогласованность: короткий URL не должен быть
# строже (Caddy basic_auth) целевого адреса, к которому и так уже нет
# basic_auth-барьера (только собственный login trade-in).
@saleshare path /sale-share /sale-share/
handle @saleshare {
redir /trade-in/sale-share permanent
@ -110,13 +157,17 @@ gendsgn.ru {
handle @tradein {
# Next.js basePath=/trade-in — фронт сам ждёт префикса в URL
reverse_proxy tradein-frontend:3000 {
header_up X-Authenticated-User {http.auth.user.id}
# #2213: симметрично с /trade-in/api/* — перезаписываем секрет из env
# (стирает клиентский), на случай SSR-forwardʼa фронтом в backend.
# См. комментарий над /trade-in/api/* выше — та же логика (явное
# удаление вместо Set с пустым {http.auth.user.id}).
header_up -X-Authenticated-User
header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
}
}
# Auth gate — с #2558 применяется ТОЛЬКО к Site Finder (handle /api/* и
# handle {} ниже). Trade-In уже отработал и short-circuit'нул выше.
import caddy/users.caddy.snippet
handle /api/* {
reverse_proxy backend:8000 {
header_up X-Authenticated-User {http.auth.user.id}