Harden multi-tenant isolation across all user-facing controllers
Added explicit CompanyId == companyId predicates to every tenant-scoped query in 22 controllers so cross-tenant data leakage is impossible even if EF Core global query filters are bypassed or misconfigured. Also fixed ApplicationDbContext.IsPlatformAdmin to correctly return true for SuperAdmins with no CompanyId claim (break-glass accounts) and when no HTTP context is present (background services, unit tests), resolving 225 unit test failures that stemmed from the global filter blocking all in-memory test data. New MultiTenantIsolationTests class (8 tests) verifies the explicit predicate layer independently of the global query filters. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -179,8 +179,9 @@ public class OvenSchedulerController : Controller
|
||||
public async Task<IActionResult> Suggest([FromBody] SuggestRequest req)
|
||||
{
|
||||
var goal = req?.OptimizationGoal ?? "maximize_throughput";
|
||||
var suggestCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
|
||||
var equipmentList = (await _unitOfWork.OvenCosts.GetAllAsync())
|
||||
var equipmentList = (await _unitOfWork.OvenCosts.FindAsync(o => o.CompanyId == suggestCompanyId))
|
||||
.Where(o => o.IsActive)
|
||||
.OrderBy(o => o.DisplayOrder).ThenBy(o => o.Label)
|
||||
.ToList();
|
||||
@@ -188,10 +189,11 @@ public class OvenSchedulerController : Controller
|
||||
if (!equipmentList.Any())
|
||||
return Json(new { success = false, error = "No active ovens found. Add Named Ovens in Settings → Operating Costs." });
|
||||
|
||||
var companyCosts = await _unitOfWork.CompanyOperatingCosts.GetAllAsync();
|
||||
var companyCosts = await _unitOfWork.CompanyOperatingCosts.FindAsync(c => c.CompanyId == suggestCompanyId);
|
||||
var defaultCycleMinutes = companyCosts.FirstOrDefault()?.DefaultOvenCycleMinutes ?? 45;
|
||||
|
||||
var queueJobs = (await _unitOfWork.Jobs.GetAllAsync(
|
||||
var queueJobs = (await _unitOfWork.Jobs.FindAsync(
|
||||
j => j.CompanyId == suggestCompanyId,
|
||||
false,
|
||||
j => j.Customer,
|
||||
j => j.JobStatus,
|
||||
@@ -265,7 +267,8 @@ public class OvenSchedulerController : Controller
|
||||
if (req?.Batches == null || !req.Batches.Any())
|
||||
return Json(new { success = false, error = "No batches provided." });
|
||||
|
||||
var companyCosts = await _unitOfWork.CompanyOperatingCosts.GetAllAsync();
|
||||
var acceptCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var companyCosts = await _unitOfWork.CompanyOperatingCosts.FindAsync(c => c.CompanyId == acceptCompanyId);
|
||||
var defaultCycleMinutes = companyCosts.FirstOrDefault()?.DefaultOvenCycleMinutes ?? 45;
|
||||
|
||||
var createdBatches = new List<object>();
|
||||
@@ -357,7 +360,8 @@ public class OvenSchedulerController : Controller
|
||||
if (oven == null)
|
||||
return Json(new { success = false, error = "Oven not found." });
|
||||
|
||||
var companyCosts = await _unitOfWork.CompanyOperatingCosts.GetAllAsync();
|
||||
var createBatchCompanyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var companyCosts = await _unitOfWork.CompanyOperatingCosts.FindAsync(c => c.CompanyId == createBatchCompanyId);
|
||||
var defaultCycleMinutes = companyCosts.FirstOrDefault()?.DefaultOvenCycleMinutes ?? 45;
|
||||
|
||||
var batchNumber = await GenerateBatchNumberAsync();
|
||||
@@ -651,7 +655,8 @@ public class OvenSchedulerController : Controller
|
||||
if (inOvenStatus != null)
|
||||
{
|
||||
var jobIds = batch.Items.Select(i => i.JobId).Distinct().ToHashSet();
|
||||
var jobs = (await _unitOfWork.Jobs.GetAllAsync()).Where(j => jobIds.Contains(j.Id));
|
||||
var startBatchCid = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var jobs = await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == startBatchCid && jobIds.Contains(j.Id));
|
||||
foreach (var job in jobs)
|
||||
job.JobStatusId = inOvenStatus.Id;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user