diff --git a/src/food-market.api/Infrastructure/Tenancy/HttpContextTenantContext.cs b/src/food-market.api/Infrastructure/Tenancy/HttpContextTenantContext.cs index ca54b9b..ad4c13e 100644 --- a/src/food-market.api/Infrastructure/Tenancy/HttpContextTenantContext.cs +++ b/src/food-market.api/Infrastructure/Tenancy/HttpContextTenantContext.cs @@ -63,18 +63,30 @@ public bool IsSuperAdmin get { if (_override.Value is { OrgId: var o }) return o; - var ctx = _accessor.HttpContext; - // SuperAdmin может прислать X-Org-Override чтобы посмотреть данные - // конкретной организации в режиме «открыть как…». Блокируем мутации - // через ReadonlyOverrideMiddleware — здесь только подменяем tenant. - if (ctx is not null && ctx.User?.IsInRole(SuperAdminRole) == true - && ctx.Request.Headers.TryGetValue(OrgOverrideHeader, out var headerVal) - && Guid.TryParse(headerVal.ToString(), out var override_)) - { - return override_; - } - var claim = ctx?.User?.FindFirst(OrganizationClaim)?.Value; + if (TryGetHttpOverrideOrg(out var http)) return http; + var claim = _accessor.HttpContext?.User?.FindFirst(OrganizationClaim)?.Value; return Guid.TryParse(claim, out var id) ? id : null; } } + + public bool IsTenantOverride + { + get + { + // AsyncLocal-override (background tasks) не считаем «открыть как…» — + // он используется в импорте/Hangfire, где и так применяется фильтр + // (IsSuper=false по умолчанию). Override-режим — только HTTP-header. + return TryGetHttpOverrideOrg(out _); + } + } + + private bool TryGetHttpOverrideOrg(out Guid orgId) + { + orgId = Guid.Empty; + var ctx = _accessor.HttpContext; + if (ctx is null) return false; + if (ctx.User?.IsInRole(SuperAdminRole) != true) return false; + if (!ctx.Request.Headers.TryGetValue(OrgOverrideHeader, out var headerVal)) return false; + return Guid.TryParse(headerVal.ToString(), out orgId); + } } diff --git a/src/food-market.application/Common/Tenancy/ITenantContext.cs b/src/food-market.application/Common/Tenancy/ITenantContext.cs index 91c083d..139cc72 100644 --- a/src/food-market.application/Common/Tenancy/ITenantContext.cs +++ b/src/food-market.application/Common/Tenancy/ITenantContext.cs @@ -5,4 +5,8 @@ public interface ITenantContext Guid? OrganizationId { get; } bool IsAuthenticated { get; } bool IsSuperAdmin { get; } + /// SuperAdmin зашёл в режим «открыто как…» через X-Org-Override. + /// В этом режиме query-filter ОБЯЗАН применяться (по выбранной orgId), + /// иначе SuperAdmin'у возвращаются записи всех орг — нарушение изоляции. + bool IsTenantOverride { get; } } diff --git a/src/food-market.infrastructure/Persistence/AppDbContext.cs b/src/food-market.infrastructure/Persistence/AppDbContext.cs index a62633b..cd0daae 100644 --- a/src/food-market.infrastructure/Persistence/AppDbContext.cs +++ b/src/food-market.infrastructure/Persistence/AppDbContext.cs @@ -116,8 +116,13 @@ protected override void OnModelCreating(ModelBuilder builder) private void ApplyTenantFilter(ModelBuilder builder) where T : class, ITenantEntity { + // SuperAdmin обходит фильтр ТОЛЬКО когда не в режиме «открыть как…». + // В override-режиме (X-Org-Override header активен) он работает в + // контексте конкретной орги — фильтр обязан применяться, иначе + // возвращаются записи всех орг и нарушается tenant isolation. builder.Entity().HasQueryFilter(e => - _tenant.IsSuperAdmin || e.OrganizationId == _tenant.OrganizationId); + (_tenant.IsSuperAdmin && !_tenant.IsTenantOverride) + || e.OrganizationId == _tenant.OrganizationId); } public override int SaveChanges() diff --git a/src/food-market.infrastructure/Persistence/DesignTimeAppDbContextFactory.cs b/src/food-market.infrastructure/Persistence/DesignTimeAppDbContextFactory.cs index 5a034b5..66fb980 100644 --- a/src/food-market.infrastructure/Persistence/DesignTimeAppDbContextFactory.cs +++ b/src/food-market.infrastructure/Persistence/DesignTimeAppDbContextFactory.cs @@ -20,5 +20,6 @@ private sealed class DesignTimeTenantContext : ITenantContext public Guid? OrganizationId => null; public bool IsAuthenticated => false; public bool IsSuperAdmin => true; + public bool IsTenantOverride => false; } }