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;
}
}