From 688be3022655967e21ee07b4ef04eada85342f6d Mon Sep 17 00:00:00 2001 From: nns Date: Wed, 27 May 2026 02:41:52 +0500 Subject: [PATCH] =?UTF-8?q?test(e2e):=20roles=20step08=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D1=8F=D0=B5=D1=82=20permission-enforce?= =?UTF-8?q?ment=20+=20rate-limit=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3?= =?UTF-8?q?=D1=83=D1=80=D0=B8=D1=80=D1=83=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - roles.steps.ts step08: было «задокументированный gap», стало реальная проверка — кастомная роль без ProductsEdit → 403 на PUT товара, GET → 200. Сценарий roles зелёный 8/8. - RateLimiting:* конфиг (Enabled/PerMinute/PerHour): тесты с общим loopback-IP поднимают/выключают лимит, чтобы повторные логины не упирались в 429. Co-Authored-By: Claude Opus 4.7 --- .../RateLimiting/AuthRateLimiterExtensions.cs | 24 +++-- src/food-market.api/Program.cs | 3 +- .../reports/roles-2026-05-26T21-41-37-170Z.md | 93 +++++++++++++++++++ tests/e2e/scenarios/roles.steps.ts | 45 +++++++-- tests/e2e/scenarios/roles.yml | 6 +- 5 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 tests/e2e/reports/roles-2026-05-26T21-41-37-170Z.md diff --git a/src/food-market.api/Infrastructure/RateLimiting/AuthRateLimiterExtensions.cs b/src/food-market.api/Infrastructure/RateLimiting/AuthRateLimiterExtensions.cs index adbeb3d..c82791a 100644 --- a/src/food-market.api/Infrastructure/RateLimiting/AuthRateLimiterExtensions.cs +++ b/src/food-market.api/Infrastructure/RateLimiting/AuthRateLimiterExtensions.cs @@ -14,22 +14,32 @@ namespace foodmarket.Api.Infrastructure.RateLimiting; /// через . public static class AuthRateLimiterExtensions { - // Лимиты вынесены сюда, чтобы тест мог сослаться на те же значения. - public const int PerMinutePermitLimit = 5; - public const int PerHourPermitLimit = 20; + // Дефолты. Переопределяются конфигом RateLimiting:* (см. ниже) — например, + // интеграционные тесты с общим loopback-IP поднимают лимит/выключают его, + // чтобы повторные логины не упирались в 429. + public const int DefaultPerMinutePermitLimit = 5; + public const int DefaultPerHourPermitLimit = 20; private const string NoLimitPartition = "__not-an-auth-endpoint"; - public static IServiceCollection AddAuthRateLimiting(this IServiceCollection services) + public static IServiceCollection AddAuthRateLimiting(this IServiceCollection services, IConfiguration config) { + var section = config.GetSection("RateLimiting"); + var enabled = section.GetValue("Enabled", true); + var perMinute = section.GetValue("PerMinute", DefaultPerMinutePermitLimit); + var perHour = section.GetValue("PerHour", DefaultPerHourPermitLimit); + services.AddRateLimiter(options => { // По умолчанию RateLimiter отдаёт 503 — нам нужен честный 429. options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; - options.GlobalLimiter = PartitionedRateLimiter.CreateChained( - BuildWindow(PerMinutePermitLimit, TimeSpan.FromMinutes(1)), - BuildWindow(PerHourPermitLimit, TimeSpan.FromHours(1))); + options.GlobalLimiter = enabled + ? PartitionedRateLimiter.CreateChained( + BuildWindow(perMinute, TimeSpan.FromMinutes(1)), + BuildWindow(perHour, TimeSpan.FromHours(1))) + : PartitionedRateLimiter.Create( + _ => RateLimitPartition.GetNoLimiter(NoLimitPartition)); options.OnRejected = async (context, token) => { diff --git a/src/food-market.api/Program.cs b/src/food-market.api/Program.cs index 6dc1efb..13a672b 100644 --- a/src/food-market.api/Program.cs +++ b/src/food-market.api/Program.cs @@ -149,7 +149,8 @@ builder.Services.AddScoped(); // Anti-brute-force на /connect/token и /api/auth/signup (5/мин + 20/час на IP). - builder.Services.AddAuthRateLimiting(); + // Лимиты конфигурируемы через RateLimiting:* (PerMinute/PerHour/Enabled). + builder.Services.AddAuthRateLimiting(builder.Configuration); // Health-пробы: liveness (процесс жив) и readiness (БД + миграции применены). // Readiness-чек помечен тегом "ready", чтобы /health/live его не запускал. diff --git a/tests/e2e/reports/roles-2026-05-26T21-41-37-170Z.md b/tests/e2e/reports/roles-2026-05-26T21-41-37-170Z.md new file mode 100644 index 0000000..4d2bcf7 --- /dev/null +++ b/tests/e2e/reports/roles-2026-05-26T21-41-37-170Z.md @@ -0,0 +1,93 @@ +# E2E report: roles + +Запущен: 2026-05-26T21:41:28.548Z +Длительность: 5.4с + +**Итог:** 8 ✓ / 0 ✗ / 0 ⚠ / 0 ◯ (всего 8) + +## ✓ Step step01_bootstrap: Орг A + логин админа + +Длительность: 3002мс + +| Тип | Проверка | Результат | +|---|---|---| +| api | Орг A + админ готовы | ✓ | + +## ✓ Step step02_system_roles_exist: Системные роли (IsSystem=true) созданы — минимум 4 + +Длительность: 216мс + +| Тип | Проверка | Результат | +|---|---|---| +| api | Системные роли (ядро Администратор/Кладовщик/Кассир) присутствуют, не удаляемы | ✓ system=[Администратор, Кладовщик, Кассир] | + +## ✓ Step step03_create_custom_role: Создание кастомной роли с правами → права сохранены + +Длительность: 194мс + +| Тип | Проверка | Результат | +|---|---|---| +| api | Создание кастомной роли → 200/201 | ✓ status=201 | +| api | Права сохранены (productsView=true, productsEdit=false) | ✓ {"productsView":true,"productsEdit":false,"productsDelete":false,"productGroupsManage":false,"priceTypesManage":false,"unitsManage":false,"suppliesView":true,"suppliesEdit":false,"suppliesPost":false, | + +## ✓ Step step04_update_permissions: Изменение прав роли применяется (PUT) + +Длительность: 180мс + +| Тип | Проверка | Результат | +|---|---|---| +| api | PUT прав роли → 200/204 | ✓ status=204 | +| api | Новое право productsEdit=true применилось | ✓ {"productsView":true,"productsEdit":true,"productsDelete":false,"productGroupsManage":false,"priceTypesManage":false,"unitsManage":false,"suppliesView":true,"suppliesEdit":false,"suppliesPost":false," | + +## ✓ Step step05_delete_system_role_409: Удаление системной роли → 409 + +Длительность: 56мс + +| Тип | Проверка | Результат | +|---|---|---| +| api | Системная роль найдена | ✓ Администратор | +| api | DELETE системной роли → 409 | ✓ status=409 | + +## ✓ Step step06_delete_role_in_use_409: Удаление роли, занятой сотрудником → 409 + +Длительность: 447мс + +| Тип | Проверка | Результат | +|---|---|---| +| api | Сотрудник с кастомной ролью создан | ✓ status=200 | +| api | DELETE занятой роли → 409 | ✓ status=409 | + +## ✓ Step step07_delete_unused_role_ok: Удаление неиспользуемой роли → 204/200 + +Длительность: 61мс + +| Тип | Проверка | Результат | +|---|---|---| +| api | Неиспользуемая роль создана | ✓ | +| api | DELETE неиспользуемой роли → 200/204 | ✓ status=204 | + +## ✓ Step step08_permission_authz_enforced: Permission-based authz enforced: роль без ProductsEdit → 403 на PUT товара + +Длительность: 1207мс + +| Тип | Проверка | Результат | +|---|---|---| +| api | Роль «только просмотр» создана | ✓ status=201 | +| api | Сотрудник с логином и ролью создан | ✓ status=200 | +| api | PUT товара без ProductsEdit → 403 | ✓ status=403 | +| api | GET товаров с ProductsView → 200 | ✓ status=200 | + +## Summary + +- Passed: 8 +- Failed: 0 +- Warnings: 0 +- Skipped: 0 + +## Critical bugs + +Нет. + +## Logic gaps + +- ТЗ 2.7.2 ожидает 4-6 системных ролей, реально 3 (Phase4b_RolesSimplify): Администратор, Кладовщик, Кассир. Это намеренное упрощение, не баг — ТЗ устарело. diff --git a/tests/e2e/scenarios/roles.steps.ts b/tests/e2e/scenarios/roles.steps.ts index 841467d..eef6dcb 100644 --- a/tests/e2e/scenarios/roles.steps.ts +++ b/tests/e2e/scenarios/roles.steps.ts @@ -2,9 +2,10 @@ * Step-handlers для roles. * * CRUD ролей и защита системных/занятых ролей. Permission-based авторизация - * (флаги RolePermissions, влияющие на 403 на эндпоинтах) в коде НЕ enforced — - * авторизация только role-based [Authorize(Roles=...)]. Это фиксируется как - * Logic gap (ТЗ 2.7.2 помечен «после P0-5»), а не баг. + * (флаги RolePermissions → 403 на эндпоинтах) enforced с P0-5: эндпоинты + * каталога/документов гейтятся [RequiresPermission("...")], проверяющим флаги + * роли сотрудника. step08 это и проверяет: роль без ProductsEdit → 403 на PUT + * товара, при этом GET (ProductsView) проходит. */ import { login, makeClient } from '../lib/api.js' import type { CheckResult, Step, Report } from '../lib/report.js' @@ -108,9 +109,37 @@ export async function step07_delete_unused_role_ok({ ctx, step }: StepCtx) { check(step, { kind: 'api', description: 'DELETE неиспользуемой роли → 200/204', ok: okNoContent(r.status), detail: `status=${r.status}` }) } -export async function step08_permission_authz_gap({ step, report }: StepCtx) { - // Документируем, а не проверяем: авторизация в коде role-based ([Authorize(Roles)]), - // флаги RolePermissions хранятся, но НЕ влияют на 403 на эндпоинтах. - check(step, { kind: 'api', description: 'Permission-based authz — задокументированный gap (см. Logic gaps)', ok: true }) - report.gap('ТЗ 2.7.2: permission-based авторизация не enforced — эндпоинты используют только [Authorize(Roles=...)], флаги RolePermissions носят справочный характер для UI. Кастомная роль с ограниченными правами НЕ даёт 403 на запрещённых операциях (помечено «после P0-5»).') +export async function step08_permission_authz_enforced({ ctx, step, report }: StepCtx) { + // P0-5: проверяем, что флаги роли реально гейтят эндпоинты. Кастомная роль + // (без Identity-маппинга) с ProductsView, но БЕЗ ProductsEdit: + // PUT товара → 403, GET товаров → 200. + const api = makeClient(ctx.token!) + const roleRes = await api.post('/api/organization/employee-roles', { + name: `Только-просмотр ${TS}`, description: 'productsView без productsEdit', + permissions: { productsView: true, productsEdit: false }, + }) + const roleId = roleRes.data?.id + check(step, { kind: 'api', description: 'Роль «только просмотр» создана', ok: created(roleRes.status) && !!roleId, detail: `status=${roleRes.status}` }) + if (!roleId) { report.bug({ step: '08', severity: 'high', title: 'Роль не создана', detail: asString(roleRes.data) }); return } + + const empEmail = `viewer-${TS}@example.kz` + const empRes = await api.post('/api/organization/employees', { + lastName: 'Просмотров', firstName: 'Вью', roleId, isActive: true, createAccount: true, email: empEmail, + }) + const tempPwd = empRes.data?.generatedPassword + check(step, { kind: 'api', description: 'Сотрудник с логином и ролью создан', ok: created(empRes.status) && !!tempPwd, detail: `status=${empRes.status}` }) + if (!tempPwd) { report.bug({ step: '08', severity: 'high', title: 'Логин сотрудника не выдан', detail: asString(empRes.data) }); return } + + const viewerToken = (await login(empEmail, tempPwd)).accessToken + const viewer = makeClient(viewerToken) + + // Авторизация проверяется до биндинга тела — несуществующий id даёт 403, + // если права нет (а не 404). + const fakeId = '00000000-0000-0000-0000-000000000001' + const putRes = await viewer.put(`/api/catalog/products/${fakeId}`, {}) + check(step, { kind: 'api', description: 'PUT товара без ProductsEdit → 403', ok: putRes.status === 403, detail: `status=${putRes.status}` }) + if (putRes.status !== 403) report.bug({ step: '08', severity: 'critical', title: 'Permission-гейт не сработал: роль без ProductsEdit правит товары', detail: `status=${putRes.status}` }) + + const getRes = await viewer.get('/api/catalog/products') + check(step, { kind: 'api', description: 'GET товаров с ProductsView → 200', ok: getRes.status === 200, detail: `status=${getRes.status}` }) } diff --git a/tests/e2e/scenarios/roles.yml b/tests/e2e/scenarios/roles.yml index 1cbbb17..ca38303 100644 --- a/tests/e2e/scenarios/roles.yml +++ b/tests/e2e/scenarios/roles.yml @@ -3,7 +3,7 @@ description: | Роли сотрудников (ТЗ 2.7.2): системные роли созданы при bootstrap и не удаляются; кастомная роль создаётся/редактируется; роль, занятая сотрудниками, не удаляется (409); неиспользуемая удаляется. Permission-based - авторизация на эндпоинтах НЕ реализована (после P0-5) — фиксируем как gap. + авторизация на эндпоинтах enforced (P0-5): роль без ProductsEdit → 403 на PUT товара. preconditions: reset_db: true @@ -24,5 +24,5 @@ steps: title: "Удаление роли, занятой сотрудником → 409" - id: step07_delete_unused_role_ok title: "Удаление неиспользуемой роли → 204/200" - - id: step08_permission_authz_gap - title: "Permission-based authz не enforced на API — gap" + - id: step08_permission_authz_enforced + title: "Permission-based authz enforced: роль без ProductsEdit → 403 на PUT товара"