fix(security): SuperAdmin edit-mode override обходит [Authorize(Roles=Admin)]
Проблема: в режиме «открыть как…» (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 <noreply@anthropic.com>
This commit is contained in:
parent
a06464baeb
commit
ab5c4c970d
|
|
@ -0,0 +1,58 @@
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace foodmarket.Api.Infrastructure.Tenancy;
|
||||||
|
|
||||||
|
/// <summary>В режиме «открыть как…» (SuperAdmin + X-Org-Override) сам
|
||||||
|
/// SuperAdmin не имеет роли Admin/Storekeeper/Cashier, потому что эти
|
||||||
|
/// роли — атрибуты сотрудника тенанта. Без трансформации claim'ов все
|
||||||
|
/// мутации, защищённые <c>[Authorize(Roles="Admin,Storekeeper")]</c>,
|
||||||
|
/// отшиваются 403 даже когда <see cref="ReadonlyOverrideMiddleware"/>
|
||||||
|
/// разрешил мутацию (edit-mode с X-Org-Override-Reason ≥ 10 символов).
|
||||||
|
///
|
||||||
|
/// Здесь мы временно (на текущий request) добавляем SuperAdmin'у полный
|
||||||
|
/// набор tenant-ролей, чтобы он мог пройти атрибуты-гарды контроллеров.
|
||||||
|
/// Изоляция и аудит остаются: query filter всё равно скоупится на
|
||||||
|
/// X-Org-Override, а <see cref="SuperAdminEditAuditFilter"/> пишет запись
|
||||||
|
/// в super_admin_audit_log с reason и diff'ом.
|
||||||
|
///
|
||||||
|
/// Срабатывает только если:
|
||||||
|
/// - User имеет роль SuperAdmin,
|
||||||
|
/// - запрос содержит заголовок X-Org-Override.
|
||||||
|
/// На GET-запросах (read-only override) тоже работает — это безопасно,
|
||||||
|
/// потому что мутации остаются за middleware-гардом.</summary>
|
||||||
|
public class SuperAdminOverrideClaimsTransformer : IClaimsTransformation
|
||||||
|
{
|
||||||
|
private readonly IHttpContextAccessor _accessor;
|
||||||
|
|
||||||
|
public SuperAdminOverrideClaimsTransformer(IHttpContextAccessor accessor)
|
||||||
|
=> _accessor = accessor;
|
||||||
|
|
||||||
|
public Task<ClaimsPrincipal> 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",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,11 @@
|
||||||
|
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddScoped<ITenantContext, HttpContextTenantContext>();
|
builder.Services.AddScoped<ITenantContext, HttpContextTenantContext>();
|
||||||
|
// ClaimsTransformation для SuperAdmin override: добавляет роли Admin/
|
||||||
|
// Storekeeper/Cashier при наличии X-Org-Override, чтобы [Authorize(Roles=...)]
|
||||||
|
// не отшивал edit-mode мутации SuperAdmin'а. См. SuperAdminOverrideClaimsTransformer.
|
||||||
|
builder.Services.AddScoped<Microsoft.AspNetCore.Authentication.IClaimsTransformation,
|
||||||
|
foodmarket.Api.Infrastructure.Tenancy.SuperAdminOverrideClaimsTransformer>();
|
||||||
|
|
||||||
builder.Services.AddDbContext<AppDbContext>(opts =>
|
builder.Services.AddDbContext<AppDbContext>(opts =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue