From 5c37fcbfad9306fc86c8c187d9b6a7e0f263a4b0 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 18 Jun 2026 16:29:18 +0300 Subject: [PATCH] =?UTF-8?q?fix(tradein):=20praktika=20brand=20only=20under?= =?UTF-8?q?=20praktika=20account=20=E2=80=94=20drop=20kopylov=20mapping=20?= =?UTF-8?q?(brand=20leak)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tradein-mvp/backend/app/core/auth.py | 16 +++++++++++----- tradein-mvp/backend/tests/test_me_brand.py | 16 +++++++++++++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/tradein-mvp/backend/app/core/auth.py b/tradein-mvp/backend/app/core/auth.py index 87792469..0dbafb3a 100644 --- a/tradein-mvp/backend/app/core/auth.py +++ b/tradein-mvp/backend/app/core/auth.py @@ -50,13 +50,19 @@ class UserScope(TypedDict): # Account → brand mapping (#657 brand-by-account) # --------------------------------------------------------------------------- # -# Пилот-аккаунты агентства «Практика» получают брендирование автоматически -# при логине (без ?brand=). Резолвится в /me как UserScope.brand. Остальные -# юзеры → None (стандартный generic UI). Slug должен существовать в таблице -# `brands` (см. services/brand.py) — иначе useBrand fallback на generic. +# Брендирование «Практика» (#657 brand-by-account) применяется ТОЛЬКО под +# аккаунтом `praktika`. Резолвится в /me как UserScope.brand, а при генерации +# trade-in PDF — по `created_by` оценки (анти-spoofing #7, бренд не хранится). +# Остальные юзеры (вкл. `kopylov`) → None (стандартный generic UI/PDF). Slug +# должен существовать в таблице `brands` (см. services/brand.py) — иначе +# useBrand fallback на generic. +# +# `kopylov` ранее мапился на бренд `praktika`, но это приводило к утечке лого +# «Практики» в отчёты, созданные под `kopylov`, не тем клиентам (инцидент: +# отчёт для Брусники с лого Практики) — маппинг удалён. NB: kopylov/praktika +# остаются в роли `pilot` в roles.yaml — это РОЛЬ, не бренд, не трогаем. _USERNAME_BRAND: dict[str, str] = { "praktika": "praktika", - "kopylov": "praktika", } diff --git a/tradein-mvp/backend/tests/test_me_brand.py b/tradein-mvp/backend/tests/test_me_brand.py index 75b4a340..f6866334 100644 --- a/tradein-mvp/backend/tests/test_me_brand.py +++ b/tradein-mvp/backend/tests/test_me_brand.py @@ -1,8 +1,8 @@ """Tests for #657 brand-by-account — UserScope.brand resolution. Pure-function tests against app.core.auth: brand slug привязывается к аккаунту -(praktika/kopylov → "praktika"), остальные юзеры → None. get_user_scope -прокидывает brand в scope, который /me отдаёт фронту. +(ТОЛЬКО praktika → "praktika"), остальные юзеры — включая kopylov — → None. +get_user_scope прокидывает brand в scope, который /me отдаёт фронту. Без БД и сети: читается реальный auth/roles.yaml (ancestor-walk внутри репо). """ @@ -29,7 +29,10 @@ from app.core.auth import get_brand_for_user, get_user_scope # noqa: E402 ("username", "expected"), [ ("praktika", "praktika"), - ("kopylov", "praktika"), + # kopylov больше НЕ брендируется как Практика (#657 brand leak fix): + # бренд резолвится по created_by при генерации PDF, поэтому утечка лого + # Практики в отчёты под kopylov исправляется тем, что kopylov → generic. + ("kopylov", None), ("admin", None), ("user1", None), ("unknown_user", None), @@ -39,6 +42,13 @@ def test_get_brand_for_user(username: str, expected: str | None) -> None: assert get_brand_for_user(username) == expected +def test_user_scope_brand_none_for_kopylov() -> None: + # Регресс-гард на #657 brand leak: kopylov-оценки рендерят generic PDF. + scope = get_user_scope("kopylov") + assert scope["brand"] is None + assert scope["username"] == "kopylov" + + def test_user_scope_carries_brand_for_praktika() -> None: scope = get_user_scope("praktika") assert scope["brand"] == "praktika"