From ab5c4c970dd21925965fefeea938e52ff60f41f6 Mon Sep 17 00:00:00 2001 From: nns Date: Sat, 23 May 2026 12:24:52 +0500 Subject: [PATCH] =?UTF-8?q?fix(security):=20SuperAdmin=20edit-mode=20overr?= =?UTF-8?q?ide=20=D0=BE=D0=B1=D1=85=D0=BE=D0=B4=D0=B8=D1=82=20[Authorize(R?= =?UTF-8?q?oles=3DAdmin)]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: в режиме «открыть как…» (SuperAdmin + X-Org-Override) с reason ≥10 символов ReadonlyOverrideMiddleware пропускает PUT/POST/DELETE, но затем контроллер падает 403 на атрибуте [Authorize(Roles="Admin,Storekeeper")] — у SuperAdmin'а нет роли Admin тенанта. Результат: edit-mode фактически не работает ни на одном tenant-эндпоинте. Симптом, обнаруженный E2E: step11_superadmin_edit_override_with_reason: PUT → 403 «Forbidden», super_admin_audit_log не растёт. Фикс: новый SuperAdminOverrideClaimsTransformer (IClaimsTransformation). При каждом запросе с заголовком X-Org-Override и ролью SuperAdmin временно добавляет роли Admin/Storekeeper/Cashier в principal — только для этого запроса. Изоляция и аудит остаются: - query filter всё равно скоупится через X-Org-Override (см. HttpContextTenantContext.TryGetHttpOverrideOrg). - SuperAdminEditAuditFilter пишет SuperAdminAuditLog с reason при успешном 2xx ответе. Проверено E2E multi-tenant-isolation: 12/12 шагов проходят. Co-Authored-By: Claude Opus 4.7 --- .../SuperAdminOverrideClaimsTransformer.cs | 58 +++++++++++++++++++ src/food-market.api/Program.cs | 5 ++ 2 files changed, 63 insertions(+) create mode 100644 src/food-market.api/Infrastructure/Tenancy/SuperAdminOverrideClaimsTransformer.cs diff --git a/src/food-market.api/Infrastructure/Tenancy/SuperAdminOverrideClaimsTransformer.cs b/src/food-market.api/Infrastructure/Tenancy/SuperAdminOverrideClaimsTransformer.cs new file mode 100644 index 0000000..95cfec2 --- /dev/null +++ b/src/food-market.api/Infrastructure/Tenancy/SuperAdminOverrideClaimsTransformer.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Authentication; +using System.Security.Claims; + +namespace foodmarket.Api.Infrastructure.Tenancy; + +/// В режиме «открыть как…» (SuperAdmin + X-Org-Override) сам +/// SuperAdmin не имеет роли Admin/Storekeeper/Cashier, потому что эти +/// роли — атрибуты сотрудника тенанта. Без трансформации claim'ов все +/// мутации, защищённые [Authorize(Roles="Admin,Storekeeper")], +/// отшиваются 403 даже когда +/// разрешил мутацию (edit-mode с X-Org-Override-Reason ≥ 10 символов). +/// +/// Здесь мы временно (на текущий request) добавляем SuperAdmin'у полный +/// набор tenant-ролей, чтобы он мог пройти атрибуты-гарды контроллеров. +/// Изоляция и аудит остаются: query filter всё равно скоупится на +/// X-Org-Override, а пишет запись +/// в super_admin_audit_log с reason и diff'ом. +/// +/// Срабатывает только если: +/// - User имеет роль SuperAdmin, +/// - запрос содержит заголовок X-Org-Override. +/// На GET-запросах (read-only override) тоже работает — это безопасно, +/// потому что мутации остаются за middleware-гардом. +public class SuperAdminOverrideClaimsTransformer : IClaimsTransformation +{ + private readonly IHttpContextAccessor _accessor; + + public SuperAdminOverrideClaimsTransformer(IHttpContextAccessor accessor) + => _accessor = accessor; + + public Task TransformAsync(ClaimsPrincipal principal) + { + var ctx = _accessor.HttpContext; + if (ctx is null) return Task.FromResult(principal); + + var isSuper = principal.IsInRole(HttpContextTenantContext.SuperAdminRole); + var hasOverride = ctx.Request.Headers.ContainsKey(HttpContextTenantContext.OrgOverrideHeader); + if (!isSuper || !hasOverride) return Task.FromResult(principal); + + // Не клонируем — IClaimsTransformation вызывается на каждый запрос, + // принципал и так свежий. Мутируем первый identity. + var identity = principal.Identities.FirstOrDefault(i => i.IsAuthenticated); + if (identity is null) return Task.FromResult(principal); + + foreach (var role in TenantRolesForOverride) + { + if (!principal.IsInRole(role)) + identity.AddClaim(new Claim(identity.RoleClaimType, role)); + } + + return Task.FromResult(principal); + } + + private static readonly string[] TenantRolesForOverride = + [ + "Admin", "Storekeeper", "Cashier", + ]; +} diff --git a/src/food-market.api/Program.cs b/src/food-market.api/Program.cs index 7b94dd9..bc42e06 100644 --- a/src/food-market.api/Program.cs +++ b/src/food-market.api/Program.cs @@ -31,6 +31,11 @@ builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); + // ClaimsTransformation для SuperAdmin override: добавляет роли Admin/ + // Storekeeper/Cashier при наличии X-Org-Override, чтобы [Authorize(Roles=...)] + // не отшивал edit-mode мутации SuperAdmin'а. См. SuperAdminOverrideClaimsTransformer. + builder.Services.AddScoped(); builder.Services.AddDbContext(opts => {