fix(s30): прод-хост в QuickLoginOptions — точное совпадение, не suffix (шаг 9/9)
Критический фикс: раньше safety-override срабатывал на host.EndsWith( ".admin.food-market.kz") — это ВКЛЮЧАЛО сам стейдж (test.admin.food-market.kz заканчивается на ".admin.food-market.kz"). Деплой на стенд это и показал: Warning в логе "test.admin.food-market.kz — это ПРОД" и /api/dev/quick- login/accounts возвращал 404. Теперь проверка: host == "admin.food-market.kz" (точное совпадение). Любой поддомен (test.admin.*, dev.admin.*) — стенд, быстрый вход включается. Если в будущем появится второй прод-хост — дописать условие явно. Также добавлено: - deploy/docker-compose.yml: пробрасывает QuickLogin__Enabled и QuickLogin__MasterPassword из env (default off). - docs/quick-login.md: как работает, предохранители, как выключить, как проверить прод. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
338877eb49
commit
ee24a5ed8e
|
|
@ -39,6 +39,13 @@ services:
|
|||
# e2e/smoke; в prod'е оставляем дефолты 3/час, 10/сутки.
|
||||
RateLimiting__SignupPerIpPerHour: ${RATE_SIGNUP_HOUR:-3}
|
||||
RateLimiting__SignupPerIpPerDay: ${RATE_SIGNUP_DAY:-10}
|
||||
# Sprint 30: «быстрый вход» на тестовом стенде. По умолчанию выключено.
|
||||
# На stage включается через QUICK_LOGIN_ENABLED=true в .env. Code
|
||||
# имеет safety-override: даже с Enabled=true, если OpenIddict__Issuer
|
||||
# смотрит на admin.food-market.kz — быстрый вход принудительно выключен
|
||||
# (см. QuickLoginOptions.EffectivelyEnabled).
|
||||
QuickLogin__Enabled: ${QUICK_LOGIN_ENABLED:-false}
|
||||
QuickLogin__MasterPassword: ${QUICK_LOGIN_MASTER_PASSWORD:-123456}
|
||||
# Host port mapping: pick free ports on existing stage server (80/443 taken by
|
||||
# legacy nginx, 5000/5002/5005 taken by legacy .NET apps).
|
||||
ports:
|
||||
|
|
|
|||
145
docs/quick-login.md
Normal file
145
docs/quick-login.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# Quick Login — быстрый вход на тестовом стенде
|
||||
|
||||
**Статус:** включено на `https://test.admin.food-market.kz` (стенд).
|
||||
**Прод:** `https://admin.food-market.kz` — выключено, попытки активации
|
||||
принудительно блокируются safety-override'ом в коде.
|
||||
|
||||
Это dev-удобство: на стенде не нужно каждый раз вводить логин+пароль.
|
||||
Выбираешь карточку аккаунта на `/login`, клик — ты внутри.
|
||||
|
||||
## Как это работает
|
||||
|
||||
### Server-side
|
||||
|
||||
Опции в `appsettings.json` / env:
|
||||
|
||||
```yaml
|
||||
QuickLogin:
|
||||
Enabled: false # default — выключено
|
||||
MasterPassword: "123456"
|
||||
```
|
||||
|
||||
Включается через env в stage-compose:
|
||||
|
||||
```
|
||||
QUICK_LOGIN_ENABLED=true
|
||||
QUICK_LOGIN_MASTER_PASSWORD=123456
|
||||
```
|
||||
|
||||
Логика работает в двух местах:
|
||||
|
||||
- **`POST /connect/token` (grant=password)** — при `EffectivelyEnabled=true`
|
||||
пароль `MasterPassword` принимается у ЛЮБОГО активного пользователя наравне
|
||||
с его собственным паролем. Константное сравнение (`CryptographicOperations.
|
||||
FixedTimeEquals`), чтобы не светить длину. Деактивированные (`IsActive=
|
||||
false`) не пускаются ни при каких условиях. `/connect/token` всё так же
|
||||
гонит через rate-limit, 2FA и org-check (архивная org отшивает).
|
||||
|
||||
- **`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 не рисует. Клик по карточке → login с
|
||||
`MasterPassword`.
|
||||
- `<StageBanner />` жёлтой полосой сверху всех страниц, когда эндпоинт
|
||||
доступен. При том же сигнале динамически добавляет `<meta name="robots"
|
||||
content="noindex,nofollow">` — чтобы Google не индексировал стенд.
|
||||
|
||||
Один web-билд используется и в проде, и на стенде — секрет на проде не
|
||||
утекает: `/api/dev/quick-login/accounts` там 404, значит блока нет.
|
||||
|
||||
## Безопасность — предохранители
|
||||
|
||||
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 тоже отбивает.** Не рискуем в неизвестном
|
||||
окружении: если Issuer пустой/мусорный — считаем выключенным.
|
||||
4. **Rate-limit не ослаблен.** Master-password идёт через ту же защиту,
|
||||
что обычный password-grant (`UseAuthFormUsernameKey` + `UseRateLimiter`).
|
||||
5. **Неактивные пользователи не пускаются.** Проверка `IsActive` сверху
|
||||
`/connect/token` — master-password это не обходит.
|
||||
6. **Startup Warning.** Program.cs логирует одно решение на старте:
|
||||
«включён для стенда» или «Enabled=true, но безопасность выключила».
|
||||
В логах прод-инстанса всегда видно, что происходит.
|
||||
|
||||
## Демо-аккаунты
|
||||
|
||||
`QuickLoginDemoSeeder` при старте с `EffectivelyEnabled=true` поднимает:
|
||||
|
||||
- Организацию **«Демо-магазин»** (идемпотентно; отдельная от «Demo Market»
|
||||
из `DevDataSeeder`, чтобы не ломать e2e).
|
||||
- Полный demo-каталог через `DemoTenantSeeder` (50 товаров / 30 продаж /
|
||||
приёмки / контрагенты).
|
||||
- Трио учёток с ролями:
|
||||
- `demo-admin@food-market.local` → `Admin`
|
||||
- `demo-cashier@food-market.local` → `Cashier`
|
||||
- `demo-keeper@food-market.local` → `Storekeeper`
|
||||
|
||||
Собственный пароль у них `Admin12345!` (унаследовано от шаблона), но
|
||||
мастер-пароль `123456` тоже работает.
|
||||
|
||||
## Чистка автотестового мусора
|
||||
|
||||
`HousekeepingJobs.PruneAutoTestEntitiesAsync` — Hangfire-recurring каждый
|
||||
день в 02:45 UTC. Удаляет автотестовые организации (regex-match по имени)
|
||||
и orphan-юзеры по email-паттерну старше 3 дней. Защищены: SuperAdmin,
|
||||
`admin@demo-market.local`, `demo-*@food-market.local`, `market@email.com`,
|
||||
«Demo Market» / «Демо-магазин».
|
||||
|
||||
Единоразовая зачистка той же логикой — `deploy/stage-prune-autotest.sql`.
|
||||
|
||||
## Как выключить обратно
|
||||
|
||||
На стенде:
|
||||
|
||||
```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, 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
|
||||
```
|
||||
|
||||
Любой другой код (200/403/500) — проблема.
|
||||
|
||||
## Файлы
|
||||
|
||||
| Что | Где |
|
||||
|---|---|
|
||||
| Опции + safety-override | `src/food-market.api/Infrastructure/QuickLoginOptions.cs` |
|
||||
| Master-password в 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 блок | `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}` |
|
||||
|
||||
## История
|
||||
|
||||
- **2026-10-04 (Sprint 30):** первоначальный ввод на тестовый стенд.
|
||||
|
|
@ -52,7 +52,10 @@ public bool EffectivelyEnabled(string? issuer, out string reason)
|
|||
return false;
|
||||
}
|
||||
var host = uri.Host.ToLowerInvariant();
|
||||
if (host == "admin.food-market.kz" || host.EndsWith(".admin.food-market.kz"))
|
||||
// ТОЛЬКО точное совпадение с прод-хостом. Любой поддомен (test.admin.*,
|
||||
// dev.admin.*) — это стенд, его трогать не надо. Если в будущем
|
||||
// появится второй прод-хост, сюда дописывается новое условие.
|
||||
if (host == "admin.food-market.kz")
|
||||
{
|
||||
reason = $"OpenIddict:Issuer host={host} — это ПРОД, быстрый вход принудительно выключен";
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Reference in a new issue