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 => {