feat(s30): QuickLoginOptions с safety-override по Issuer (шаг 1/9)
Dev-only флаг «быстрого входа». Enabled=false по умолчанию, подхватывает QuickLogin:Enabled и QuickLogin:MasterPassword из конфига (планируется проброс через env в stage-compose). EffectivelyEnabled() — жёсткий предохранитель: даже с Enabled=true, если OpenIddict:Issuer указывает на admin.food-market.kz (прод) — считаем выключенным. Пустой Issuer тоже отбивает — не рискуем в неизвестном окружении. Program.cs логирует Warning на старте: включили → «QuickLogin включён для стенда», Enabled=true но прод → «не активирован», Enabled=false — молчим. Чтобы ошибка конфигурации всегда была видна в логах прод-инстанса. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8572875e7f
commit
1f70d90485
61
docs/quick-login-progress.md
Normal file
61
docs/quick-login-progress.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Quick Login — прогресс
|
||||
|
||||
Задача владельца: на `test.admin.food-market.kz` не набирать логин/пароль руками.
|
||||
Выбрать аккаунт из списка, пароль у всех один — `123456`. На `admin.food-market.kz`
|
||||
этого нет никогда (несколько независимых предохранителей).
|
||||
|
||||
Координатор отдельно проверил: на стейдже нет реальных секретов (МойСклад
|
||||
токенов нет, ОФД-ключей нет, Telegram не привязан, SMTP пустой) — потому
|
||||
открытый мастер-пароль на стенде безопасен.
|
||||
|
||||
## Чек-лист
|
||||
|
||||
- [x] 1. **Флаг конфигурации** — `QuickLogin:Enabled` (default false) +
|
||||
`QuickLogin:MasterPassword` (default `123456`). Включается только через
|
||||
env-переменные `QuickLogin__Enabled=true` в stage-compose. Жёсткий
|
||||
предохранитель: если `OpenIddict:Issuer` указывает на admin.food-market.kz —
|
||||
быстрый вход принудительно выключен и в лог Warning.
|
||||
- [ ] 2. **Мастер-пароль** — password-grant: `QuickLogin:MasterPassword`
|
||||
работает как второй вход рядом со своим паролем. Неактивные/без-org
|
||||
пользователи — отшиваются как раньше. Rate-limit не ослаблять.
|
||||
- [ ] 3. **API списка аккаунтов** — `GET /api/dev/quick-login/accounts`
|
||||
(анонимный, 404 если выключен). Порядок: SuperAdmin → демо-* → остальные
|
||||
«человеческие». Autotest-патéрны (cat31-*, visual-*, factory-smoke-*, mt6*,
|
||||
quality-*, i18n*-*, auth2*-*, doc4*-*, rep5*-*, rt8*-*, twofa*-*, wiz9*-*,
|
||||
a11y-*, mob1*-*, isoB*-*, axdet-*, smoke-*, ratetest-*, perf-*, probe-*,
|
||||
load-*, s19-test-* и всё с суффиксом из ≥10 цифр) — скрыты. Лимит 50 + `?q=`.
|
||||
- [ ] 4. **Демо-аккаунты** — идемпотентный сид «Демо-магазин» (через
|
||||
существующий `DemoTenantSeeder`) + сотрудники с ролями Admin / Cashier /
|
||||
Storekeeper (`demo-admin@food-market.local`, `demo-cashier@...`,
|
||||
`demo-keeper@...`). Запускается только при Enabled=true.
|
||||
- [ ] 5. **Страница входа (web)** — если `GET .../accounts` вернул 200 — над
|
||||
обычной формой блок «Быстрый вход (тестовый стенд)» с карточками + поиск.
|
||||
Клик — моментальный логин через мастер-пароль. На проде блока нет.
|
||||
- [ ] 6. **Плашка «ТЕСТОВЫЙ СТЕНД»** — при Enabled=true жёлтая полоса
|
||||
сверху + `<meta robots="noindex,nofollow">` в index.html стенда.
|
||||
- [ ] 7. **Чистка автотестового мусора** — SQL-скрипт для стейджа + расширение
|
||||
`HousekeepingJobs.PruneQualityTestOrgsAsync` на новые autotest-патéрны,
|
||||
чтобы cron 02:30 UTC гасил новые юзеры сам. Не трогать: SuperAdmin,
|
||||
admin@demo-market.local, market@email.com, demo-* аккаунты.
|
||||
- [ ] 8. **Тесты** — integration (3): Enabled=false → эндпоинт 404 + мастер-
|
||||
пароль отвергается; Issuer=admin.food-market.kz → выключен даже с
|
||||
Enabled=true; Enabled=true → мастер-пароль пускает активного и не пускает
|
||||
неактивного. E2E stage: клик по демо-кассиру → меню кассира. Прод: `curl
|
||||
https://admin.food-market.kz/api/dev/quick-login/accounts` → 404.
|
||||
- [ ] 9. **Деплой + документация** — поднять env в stage-compose
|
||||
(локальный репо + prod-vm `~/food-market-stage/deploy/docker-compose.yml`),
|
||||
`~/deploy-stage.sh`, ретест, `docs/quick-login.md`.
|
||||
|
||||
## Правила (сам себе)
|
||||
|
||||
- Мастер-пароль и список аккаунтов — **никогда не на проде**. По умолчанию
|
||||
выключено, Issuer=admin.* — выключено независимо от флага.
|
||||
- После каждого пункта: build + соответствующие тесты + коммит порцией +
|
||||
`[x]` в этом файле.
|
||||
- Не трогать: прод admin.food-market.kz (кроме read-only проверки 404),
|
||||
`global.json`, POS WPF, gateway nginx.
|
||||
|
||||
## Условие остановки
|
||||
|
||||
9 пунктов `[x]`, тесты зелёные, стейдж задеплоен, прод-эндпоинт 404 →
|
||||
итог в этом файле + `touch ~/.fm-watchdog/DONE`.
|
||||
83
src/food-market.api/Infrastructure/QuickLoginOptions.cs
Normal file
83
src/food-market.api/Infrastructure/QuickLoginOptions.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
namespace foodmarket.Api.Infrastructure;
|
||||
|
||||
/// <summary>Опции «быстрого входа» на тестовом стенде. По умолчанию выключен.
|
||||
/// Включается ТОЛЬКО переменной окружения <c>QuickLogin__Enabled=true</c>
|
||||
/// в stage-compose. В прод-compose этой переменной нет — флаг остаётся
|
||||
/// в false и ни эндпоинт списка аккаунтов, ни «мастер-пароль» не включаются.
|
||||
///
|
||||
/// Жёсткий предохранитель <see cref="EffectivelyEnabled"/>: даже если флаг
|
||||
/// случайно пробросили в прод, но <c>OpenIddict:Issuer</c> указывает на
|
||||
/// admin.food-market.kz — быстрый вход всё равно выключен. Это защищает от
|
||||
/// ошибок конфигурации (ошибочный set-env на prod-env) — конфиг-опечатка
|
||||
/// не должна открыть прод.</summary>
|
||||
public sealed class QuickLoginOptions
|
||||
{
|
||||
/// <summary>Главный флаг. При false любой код, относящийся к quick-login,
|
||||
/// должен вести себя как в проде (эндпоинт 404, мастер-пароль не работает,
|
||||
/// плашка не рендерится).</summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>Единый пароль-ключ для стенда. При включённом флаге подходит
|
||||
/// к ЛЮБОМУ активному аккаунту в <c>/connect/token</c> grant=password.
|
||||
/// Собственный пароль пользователя тоже продолжает работать (чтобы не
|
||||
/// ломать e2e, seeded admin и т.д.).</summary>
|
||||
public string MasterPassword { get; set; } = "123456";
|
||||
|
||||
/// <summary>Эффективный флаг с учётом safety-override по Issuer. Используй
|
||||
/// везде, где решается «пускать мастер-пароль / отдавать список». Чтение
|
||||
/// сравнивает Issuer с prod-доменом admin.food-market.kz — если совпадает
|
||||
/// (или Issuer пустой), быстрый вход считается выключенным и в лог
|
||||
/// пишется Warning один раз на старте через <see cref="LogSafetyDecision"/>.</summary>
|
||||
public bool EffectivelyEnabled(string? issuer, out string reason)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
reason = "QuickLogin:Enabled=false";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Issuer обязателен — если его нет, не рискуем: dev/stage без явного
|
||||
// Issuer не должны открывать мастер-пароль, иначе любое неверно
|
||||
// настроенное окружение превращается в стенд.
|
||||
if (string.IsNullOrWhiteSpace(issuer))
|
||||
{
|
||||
reason = "OpenIddict:Issuer пустой — не могу подтвердить, что это стенд";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Прод-домен в lower-case: Uri сам нормализует хост, но подстрахуем.
|
||||
if (!Uri.TryCreate(issuer, UriKind.Absolute, out var uri))
|
||||
{
|
||||
reason = $"OpenIddict:Issuer '{issuer}' не парсится как Uri";
|
||||
return false;
|
||||
}
|
||||
var host = uri.Host.ToLowerInvariant();
|
||||
if (host == "admin.food-market.kz" || host.EndsWith(".admin.food-market.kz"))
|
||||
{
|
||||
reason = $"OpenIddict:Issuer host={host} — это ПРОД, быстрый вход принудительно выключен";
|
||||
return false;
|
||||
}
|
||||
|
||||
reason = $"OpenIddict:Issuer host={host} — стенд, быстрый вход включён";
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Одноразовый лог при старте приложения. Пишется в лог, чтобы
|
||||
/// в проде всегда было видно: «флаг пришёл, но Issuer=prod, выключили».
|
||||
/// Вызывается из Program.cs после Build().</summary>
|
||||
public void LogSafetyDecision(ILogger logger, string? issuer)
|
||||
{
|
||||
var effective = EffectivelyEnabled(issuer, out var reason);
|
||||
if (effective)
|
||||
{
|
||||
logger.LogWarning("QuickLogin включён для стенда: {Reason}. " +
|
||||
"Это норма для test.admin.food-market.kz. В проде должно быть выключено.", reason);
|
||||
}
|
||||
else if (Enabled)
|
||||
{
|
||||
// Enabled=true, но effective=false — явный safety-переключатель. Это серьёзно.
|
||||
logger.LogWarning("QuickLogin:Enabled=true, но быстрый вход НЕ активирован. {Reason}", reason);
|
||||
}
|
||||
// Если Enabled=false — молчим, это обычный прод.
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,11 @@
|
|||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<ITenantContext, HttpContextTenantContext>();
|
||||
|
||||
// Sprint 30: «быстрый вход» на тестовом стенде. Dev-only флаг, выключен
|
||||
// по умолчанию, с safety-override по Issuer (см. QuickLoginOptions).
|
||||
builder.Services.Configure<foodmarket.Api.Infrastructure.QuickLoginOptions>(
|
||||
builder.Configuration.GetSection("QuickLogin"));
|
||||
|
||||
// Sprint 20 / TD-3: Mapster config — singleton TypeAdapterConfig.
|
||||
// Используется в контроллерах через ProjectToType<TDto>(MapsterConfig.Config)
|
||||
// для SQL-friendly проекций. IMapper в DI не регистрируем, потому что
|
||||
|
|
@ -477,6 +482,17 @@ [new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
|||
|
||||
var app = builder.Build();
|
||||
|
||||
// Sprint 30: один раз на старте проверяем решение по quick-login
|
||||
// (Enabled+Issuer). Пишет Warning и в прод-env, и в стенде, чтобы в логах
|
||||
// всегда было видно, в каком режиме стартанули.
|
||||
{
|
||||
var ql = app.Services.GetRequiredService<Microsoft.Extensions.Options.IOptions<
|
||||
foodmarket.Api.Infrastructure.QuickLoginOptions>>().Value;
|
||||
var issuer = builder.Configuration["OpenIddict:Issuer"];
|
||||
ql.LogSafetyDecision(app.Services.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger("QuickLogin"), issuer);
|
||||
}
|
||||
|
||||
// HSTS — только когда мы за HTTPS (stage/prod через nginx). В Development
|
||||
// обычно работаем по http://localhost:5081, и Strict-Transport-Security
|
||||
// прибил бы локальный браузер на год. По дефолту 365 дней + preload.
|
||||
|
|
|
|||
Loading…
Reference in a new issue