Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Public / Build + push Public (test + latest) (push) Waiting to run
Docker Public / Deploy :test → test.food-market.kz (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Координатор отротировал мастер-пароль стенда: теперь значение живёт в ~/.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>
191 lines
11 KiB
Markdown
191 lines
11 KiB
Markdown
# 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:
|
||
|
||
```yaml
|
||
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`
|
||
принимает ДВА варианта пароля наравне с собственным паролем юзера:
|
||
1. **MasterPassword** (значение env'а) — для ручного/скриптового
|
||
входа, когда у тебя есть сам пароль. Константное сравнение
|
||
(`FixedTimeEquals`).
|
||
2. **Sentinel** — публичная строка `__QUICK_LOGIN_PWLESS__`, которую
|
||
шлёт SPA при клике по карточке. Сервер трактует её как «пустить
|
||
этого юзера». Это позволяет SPA не хранить реальный пароль в
|
||
JS-бандле.
|
||
И тот и другой путь гейтованы `EffectivelyEnabled` — вне стенда оба
|
||
отфутболиваются как любой другой чужой пароль.
|
||
|
||
- **`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 сервером всё равно отвергается.
|
||
|
||
## Безопасность — предохранители
|
||
|
||
1. **Enabled=false по умолчанию.** Нужна явная env-переменная, чтобы
|
||
включить. В прод-compose её нет и не должно быть.
|
||
2. **Issuer safety-override.** В `QuickLoginOptions.EffectivelyEnabled`:
|
||
если `OpenIddict:Issuer` указывает на `admin.food-market.kz` (ТОЧНО,
|
||
без поддоменов), быстрый вход **принудительно выключен**. Защита от
|
||
ошибки конфигурации — поставили `QUICK_LOGIN_ENABLED=true` в прод-env
|
||
по ошибке, но Issuer остался прод-овым → ничего не включилось, в
|
||
лог Warning.
|
||
3. **Пустой или кривой Issuer тоже отбивает.** Не рискуем в неизвестном
|
||
окружении.
|
||
4. **Пустой MasterPassword отбивает мастер-пароль.** Пустая строка не
|
||
матчится в `IsMasterPassword` (отбивается до FixedTimeEquals). Это
|
||
защищает от случайно разворачиваемого стенда без секрета.
|
||
5. **Rate-limit не ослаблен.** Sentinel и master-password идут через ту
|
||
же защиту, что обычный password-grant.
|
||
6. **Неактивные пользователи не пускаются.** Проверка `IsActive` сверху
|
||
`/connect/token` — ни sentinel, ни master-password это не обходят.
|
||
7. **Startup Warning.** Program.cs логирует одно решение на старте:
|
||
«включён для стенда» или «Enabled=true, но безопасность выключила».
|
||
8. **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` → `Admin`
|
||
- `demo-cashier@food-market.local` → `Cashier`
|
||
- `demo-keeper@food-market.local` → `Storekeeper`
|
||
|
||
Собственный пароль у них `Admin12345!` (унаследовано от шаблона), но
|
||
sentinel и master-password тоже работают.
|
||
|
||
## Как ротирнуть секрет-пароль
|
||
|
||
1. На dev-vm обновить `~/.fm-watchdog/stage-shared-password` (mode 600).
|
||
2. На prod-vm обновить `~/food-market-stage/deploy/.env`:
|
||
`QUICK_LOGIN_MASTER_PASSWORD=<новое значение>`. Удобно без эха:
|
||
```bash
|
||
# с 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'
|
||
```
|
||
3. На prod-vm также обновить basic-auth htpasswd для `test.admin.food-
|
||
market.kz` в nginx — тот же пароль с юзером `food-market`.
|
||
4. Рестартнуть API: `ssh nns@192.168.1.190 "cd ~/food-market-stage/deploy
|
||
&& docker compose -p food-market-stage up -d --force-recreate api"`.
|
||
5. Веб rebuild **не нужен** — sentinel не меняется.
|
||
|
||
## Как выключить обратно
|
||
|
||
```bash
|
||
# на 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`.
|
||
|
||
## Проверить что прод не задет
|
||
|
||
```bash
|
||
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 бьют без
|
||
пароля.
|