Sprint 13 — security + observability deep. 7 пунктов чек-листа ✓.
Подробности — docs/sprint13-progress.md и docs/food-market-server-postgres-role.md.
Главное:
- food-market-server (back.food-market.kz, legacy backend) теперь
работает на dedicated PG-роли food_market_server_app (NOSUPERUSER /
NOCREATEDB / NOCREATEROLE / NOREPLICATION / NOBYPASSRLS) с CRUD-only
грантами. Раньше использовался postgres-superuser с паролем 1q2w3e4r.
Бэкап конфига сохранён, rollback одной командой.
- SecurityHeadersMiddleware навешивает CSP / X-Frame-Options DENY /
X-Content-Type-Options nosniff / Referrer-Policy strict-origin /
Permissions-Policy. HSTS 365d + includeSubDomains + preload.
Те же заголовки в deploy/nginx.conf для SPA HTML.
- Rate-limit:
• Signup-IP — 3/час + 10/день (на stage'е переопределено через
.env RATE_SIGNUP_HOUR=30 чтобы не ломать e2e).
• Forgot-password — per-email 3/час + per-IP 10/час.
- SensitiveOpsAudit сервис, wired в:
• TwoFactor enroll/disable
• Employees.Update при смене RoleId (action=AssignRole,
payload с prev/next role + полный RolePermissions)
• MeAccount.ChangePassword (новый endpoint)
• MeSessions.RevokeAll (новый endpoint)
- POST /api/me/sessions/revoke-all — через
IOpenIddictAuthorizationManager.FindBySubjectAsync + TryRevokeAsync.
Integration-тест: refresh после revoke → 400/401.
- Hangfire dashboard — nginx-route добавлен (раньше /hangfire ловился
SPA-fallback'ом). Фильтр SuperAdmin'ом уже был. Тест: anon/tenant →
401/403/404.
- Grafana dashboard JSON (deploy/grafana/dashboards/food-market.json,
9 панелей) + инструкции импорта в docs/observability.md.
Проверено на stage'е: все 6 security-заголовков видны на /;
/hangfire → 401 (закрыт); 4-я форгот → 429; stage-smoke (5 этапов) ✓.
Тесты: 68 unit + 9 integration (включая 3 новых: SessionRevokeTests,
HangfireAccessTests).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
49 lines
2.4 KiB
C#
49 lines
2.4 KiB
C#
using System.Net;
|
||
using System.Net.Http.Headers;
|
||
using FluentAssertions;
|
||
using foodmarket.IntegrationTests.Support;
|
||
using Xunit;
|
||
|
||
namespace foodmarket.IntegrationTests;
|
||
|
||
/// <summary>Sprint 13: Hangfire Dashboard защищён <c>SuperAdminHangfireFilter</c>.
|
||
///
|
||
/// ВАЖНО: в тестовом окружении ApiFactory выставляет
|
||
/// <c>Hangfire__Enabled=false</c> (иначе Hangfire-сервер плодил бы свои
|
||
/// таблицы в одноразовом контейнере). Когда сервер выключен,
|
||
/// <c>app.UseHangfireDashboard</c> не вызывается → /hangfire отдаёт 404.
|
||
///
|
||
/// Поэтому в этих тестах мы проверяем «дашборд НЕ открыт без авторизации»:
|
||
/// допустимый результат — 401, 403 или 404 (любая форма «нет доступа»).
|
||
/// SuperAdmin-кейс не проверяем здесь: для него на stage есть e2e-проверка
|
||
/// (см. stage-smoke). Это компромисс ради не-зависимости теста от
|
||
/// Hangfire-сервера.</summary>
|
||
[Collection(ApiCollection.Name)]
|
||
public class HangfireAccessTests
|
||
{
|
||
private readonly ApiFactory _factory;
|
||
public HangfireAccessTests(ApiFactory factory) => _factory = factory;
|
||
|
||
[Fact]
|
||
public async Task Anonymous_hangfire_returns_unauthorized()
|
||
{
|
||
using var client = _factory.CreateClient();
|
||
var resp = await client.GetAsync("/hangfire");
|
||
// Hangfire dashboard middleware возвращает 401 без auth — это NOT a 200
|
||
// (если бы было 200 — это значит filter не сработал и /hangfire утёк).
|
||
((int)resp.StatusCode).Should().BeOneOf(401, 403, 404);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Tenant_admin_cannot_access_hangfire()
|
||
{
|
||
var actor = new ApiActor(_factory.CreateClient());
|
||
await actor.SignupAndLoginAsync($"ha-{Guid.NewGuid():N}");
|
||
var resp = await actor.Http.GetAsync("/hangfire");
|
||
// 401/403 — фильтр сработал; 404 — Hangfire выключен в тестах
|
||
// (нет UseHangfireDashboard'а). Любой из этих кодов — это «доступа нет»,
|
||
// именно то что мы хотим проверить.
|
||
((int)resp.StatusCode).Should().BeOneOf(401, 403, 404);
|
||
}
|
||
}
|