Координатор отротировал мастер-пароль стенда: теперь значение живёт в ~/.fm-watchdog/stage-shared-password на dev-vm (не коммитится) и в QUICK_LOGIN_MASTER_PASSWORD в stage-env на prod-vm. Старый дефолт '123456' на обоих сторонах убран — он мог вводить в заблуждение и был записан в бандл. Клиент-серверный контракт: - Server (AuthorizationController.IsMasterPassword) принимает ДВА пароля как мастер-ключ: (a) QuickLoginOptions.MasterPassword (константное сравнение, FixedTimeEquals); (b) публичный sentinel QuickLoginOptions.SentinelPassword = '__QUICK_LOGIN_PWLESS__'. Оба гейтованы EffectivelyEnabled — вне стенда отбиваются как любой чужой пароль. - SPA (LoginPage.handleQuickLogin) при клике карточки шлёт sentinel в поле password. Реальный пароль в JS-бандл больше не попадает; ротация секрета больше не требует rebuild'а веба. - Форму мы НЕ наполняем sentinel'ом — поле password остаётся пустым, чтобы ни одно значение не светилось в DOM. Default QuickLoginOptions.MasterPassword = "" (пустая строка не матчится в IsMasterPassword). deploy/docker-compose.yml дефолт QUICK_LOGIN_MASTER_PASSWORD тоже пустой. Тесты: - integration (3/3 ok) — добавлены sentinel-кейсы: пропускает активного при Enabled=true, отвергается при Enabled=false. Admin12345!-санити- логин в Disabled-тесте перенесён вперёд, чтобы снять lockout-счётчик от соседей по коллекции (Identity lockoutOnFailure=true). - e2e stage-quick-login (4/4 ok) — ql02 шлёт sentinel вместо '123456' (симулирует реальный SPA flow). docs/quick-login.md: - без упоминания реального пароля; хранилище — ~/.fm-watchdog/ stage-shared-password; ротация-рецепт через python читает файл в-процессе (не экспонирует значение в ps/логи). - раздел «Доступ к стенду» — host-nginx basic-auth для внешнего интернета, 192.168.1.x бьёт без неё. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
11 KiB
Quick Login — быстрый вход на тестовом стенде
Статус: включено на https://test.admin.food-market.kz (стенд).
Прод: https://admin.food-market.kz — выключено, попытки активации
принудительно блокируются safety-override'ом в коде.
Это dev-удобство: на стенде не нужно каждый раз вводить логин+пароль.
Выбираешь карточку аккаунта на /login, клик — ты внутри.
Доступ к стенду
Стенд test.admin.food-market.kz для публичного интернета закрыт
HTTP basic-auth на host-уровне nginx (логин food-market). Из внутренней
сети 192.168.1.x (dev-vm, prod-vm) basic-auth не требуется — e2e,
quality-watchdog и обычная разработка работают как раньше.
Пароль basic-auth = пароль quick-login (чтобы не плодить секреты). Хранится
на dev-vm в файле ~/.fm-watchdog/stage-shared-password (mode 600, не
коммитится).
Как это работает
Server-side
Опции в appsettings.json / env:
QuickLogin:
Enabled: false # default — выключено
MasterPassword: "" # default пустой; реальный секрет — ТОЛЬКО в stage-env
На стенде env в ~/food-market-stage/deploy/.env:
QUICK_LOGIN_ENABLED=true
QUICK_LOGIN_MASTER_PASSWORD=<значение из ~/.fm-watchdog/stage-shared-password>
Пароль прокидывается в контейнер build'ом из файла и в репозиторий не попадает. Прод-compose этой переменной не содержит, флаг остаётся false.
Логика срабатывает в двух местах:
-
POST /connect/token(grant=password) — приEffectivelyEnabled=trueпринимает ДВА варианта пароля наравне с собственным паролем юзера:- MasterPassword (значение env'а) — для ручного/скриптового
входа, когда у тебя есть сам пароль. Константное сравнение
(
FixedTimeEquals). - Sentinel — публичная строка
__QUICK_LOGIN_PWLESS__, которую шлёт SPA при клике по карточке. Сервер трактует её как «пустить этого юзера». Это позволяет SPA не хранить реальный пароль в JS-бандле. И тот и другой путь гейтованыEffectivelyEnabled— вне стенда оба отфутболиваются как любой другой чужой пароль.
- MasterPassword (значение env'а) — для ручного/скриптового
входа, когда у тебя есть сам пароль. Константное сравнение
(
-
GET /api/dev/quick-login/accounts— анонимный эндпоинт, который возвращает «человеческие» аккаунты для карточек: SuperAdmin → demo-trio → остальные. Автотестовые логины отсеиваются по regex-патéрнам (всё с суффиксом из 10+ цифр или prefix cat31-/visual-/factory-smoke-/mt6*/ quality-/...). Поиск?q=фильтрует по email/ФИО/роли/магазину. На проде эндпоинт отдаёт 404 — наличие пути не палит окружение.
Client-side (SPA)
<QuickLoginBlock />над формой логина сам опрашивает/api/dev/quick- login/accounts. 200 → рендерит жёлтый блок с карточками + поиск. 404 → возвращаетnull, никакого UI не рисует.- Клик по карточке →
POST /connect/tokenс паролем sentinel__QUICK_LOGIN_PWLESS__. Реальный мастер-пароль в бандл не попадает — ротация пароля в env не требует rebuild'а веба. <StageBanner />жёлтой полосой сверху всех страниц, когда эндпоинт доступен. При том же сигнале динамически добавляет<meta name="robots" content="noindex,nofollow">.
Один web-билд используется и в проде, и на стенде — секрет на проде не
утекает: /api/dev/quick-login/accounts там 404, значит блока нет, а
sentinel сервером всё равно отвергается.
Безопасность — предохранители
- Enabled=false по умолчанию. Нужна явная env-переменная, чтобы включить. В прод-compose её нет и не должно быть.
- Issuer safety-override. В
QuickLoginOptions.EffectivelyEnabled: еслиOpenIddict:Issuerуказывает наadmin.food-market.kz(ТОЧНО, без поддоменов), быстрый вход принудительно выключен. Защита от ошибки конфигурации — поставилиQUICK_LOGIN_ENABLED=trueв прод-env по ошибке, но Issuer остался прод-овым → ничего не включилось, в лог Warning. - Пустой или кривой Issuer тоже отбивает. Не рискуем в неизвестном окружении.
- Пустой MasterPassword отбивает мастер-пароль. Пустая строка не
матчится в
IsMasterPassword(отбивается до FixedTimeEquals). Это защищает от случайно разворачиваемого стенда без секрета. - Rate-limit не ослаблен. Sentinel и master-password идут через ту же защиту, что обычный password-grant.
- Неактивные пользователи не пускаются. Проверка
IsActiveсверху/connect/token— ни sentinel, ни master-password это не обходят. - Startup Warning. Program.cs логирует одно решение на старте: «включён для стенда» или «Enabled=true, но безопасность выключила».
- Basic auth на host-nginx. Внешние запросы к
test.admin.food- market.kzотбиваются 401 без корректных basic-auth creds. Внутренняя сеть исключена из этого gate'а (чтобы автоматика работала). Этот уровень живёт за пределами репозитория — в gateway nginx на prod-vm.
Демо-аккаунты
QuickLoginDemoSeeder при старте с EffectivelyEnabled=true поднимает:
- Организацию «Демо-магазин» (идемпотентно).
- Полный demo-каталог через
DemoTenantSeeder(50 товаров / 30 продаж / приёмки / контрагенты). Если демо-каталог не наполнился (нет HTTP- контекста при startup →Tenant not set), юзеры всё равно создаются. - Трио учёток с ролями:
demo-admin@food-market.local→Admindemo-cashier@food-market.local→Cashierdemo-keeper@food-market.local→Storekeeper
Собственный пароль у них Admin12345! (унаследовано от шаблона), но
sentinel и master-password тоже работают.
Как ротирнуть секрет-пароль
- На dev-vm обновить
~/.fm-watchdog/stage-shared-password(mode 600). - На prod-vm обновить
~/food-market-stage/deploy/.env:QUICK_LOGIN_MASTER_PASSWORD=<новое значение>. Удобно без эха:# с dev-vm scp ~/.fm-watchdog/stage-shared-password nns@192.168.1.190:/tmp/.ql-pw ssh nns@192.168.1.190 'python3 -c " import pathlib, re pw = pathlib.Path(\"/tmp/.ql-pw\").read_text().strip() env = pathlib.Path(\"/home/nns/food-market-stage/deploy/.env\") env.write_text(re.sub(r\"^QUICK_LOGIN_MASTER_PASSWORD=.*$\", f\"QUICK_LOGIN_MASTER_PASSWORD={pw}\", env.read_text(), flags=re.M))" \ && rm /tmp/.ql-pw && grep -c QUICK_LOGIN_MASTER_PASSWORD \ ~/food-market-stage/deploy/.env' - На prod-vm также обновить basic-auth htpasswd для
test.admin.food- market.kzв nginx — тот же пароль с юзеромfood-market. - Рестартнуть API:
ssh nns@192.168.1.190 "cd ~/food-market-stage/deploy && docker compose -p food-market-stage up -d --force-recreate api". - Веб rebuild не нужен — sentinel не меняется.
Как выключить обратно
# на prod-vm
nns@192.168.1.190:$ sed -i '/^QUICK_LOGIN_ENABLED=/d; /^QUICK_LOGIN_MASTER_PASSWORD=/d' \
~/food-market-stage/deploy/.env
nns@192.168.1.190:$ cd ~/food-market-stage/deploy && docker compose -p food-market-stage up -d --force-recreate api
API перестартует, EffectivelyEnabled увидит Enabled=false, эндпоинт
снова 404, sentinel и master-password не работают. Карточки на /login
исчезнут — QuickLoginBlock увидит 404 и вернёт null.
Проверить что прод не задет
curl -s -o /dev/null -w "%{http_code}\n" \
https://admin.food-market.kz/api/dev/quick-login/accounts
# ожидается: 404 (или 502, если прод-API вообще не поднят)
Любой 200 — проблема.
Файлы
| Что | Где |
|---|---|
| Опции + safety-override + sentinel-константа | src/food-market.api/Infrastructure/QuickLoginOptions.cs |
| Master-password + sentinel в token-grant | src/food-market.api/Controllers/AuthorizationController.cs |
| Список аккаунтов | src/food-market.api/Controllers/QuickLoginController.cs |
| Демо-сид | src/food-market.api/Seed/QuickLoginDemoSeeder.cs |
| Чистка автотестов | src/food-market.api/Background/HousekeepingJobs.cs + deploy/stage-prune-autotest.sql |
| UI блок (sentinel-flow) | src/food-market.web/src/pages/LoginPage.tsx + src/food-market.web/src/components/QuickLoginBlock.tsx |
| Плашка стенда | src/food-market.web/src/components/StageBanner.tsx |
| Integration-тесты | tests/food-market.IntegrationTests/QuickLoginTests.cs |
| E2E-сценарий | tests/e2e/scenarios/stage-quick-login.{yml,steps.ts} |
| Секрет-пароль (на dev-vm) | ~/.fm-watchdog/stage-shared-password (вне репо) |
История
- 2026-10-04 (Sprint 30, часть 1): первоначальный ввод на тестовый стенд.
- 2026-10-04 (Sprint 30, часть 2): секрет-пароль вынесен в
~/.fm- watchdog/stage-shared-password(вне репо); SPA перешла на sentinel — реальный пароль в JS-бандл больше не попадает; стенд закрыт basic-auth на host-nginx для внешнего интернета, внутренние 192.168.1.x бьют без пароля.