Критический фикс: раньше 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>
87 lines
5.4 KiB
C#
87 lines
5.4 KiB
C#
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 — молчим, это обычный прод.
|
||
}
|
||
}
|