food-market/src/food-market.api/Infrastructure/QuickLoginOptions.cs
nns ee24a5ed8e 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>
2026-10-04 16:22:18 +05:00

87 lines
5.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
// ТОЛЬКО точное совпадение с прод-хостом. Любой поддомен (test.admin.*,
// dev.admin.*) — это стенд, его трогать не надо. Если в будущем
// появится второй прод-хост, сюда дописывается новое условие.
if (host == "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 — молчим, это обычный прод.
}
}