From 7c0357b4c50f24fb10a2fb35fb2dbeab3dfbcc8d Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Sat, 20 Jun 2026 10:52:45 -0400 Subject: [PATCH] Add explicit CompanyId filter to tenant-scoped GetAllAsync calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-tenant defense-in-depth sweep (GetAllAsync vector). These queries relied solely on the global tenant query filter, so a SuperAdmin session (filter bypassed) would have merged/exported every tenant's data. Now each filters explicitly by CompanyId: - ToolsController: all 11 CSV export methods (Customers, Quotes, Jobs, Invoices, Payments, Purchase Orders x2, Inventory, Maintenance, Appointments, Expenses) — converted GetAllAsync -> FindAsync(CompanyId). Highest impact: these are bulk data exports. - KioskController.Intakes: kiosk sessions (+ linked customer/job). - BillsController recurring-bill detection: companyId was computed but never applied to the query. Verified safe (no change): the ignoreQueryFilters cases (SKU-sequence generators read only SKU strings; the SuperAdmin category repair), OvenScheduler (indirectly scoped by the company's job-item ids), and the platform/SuperAdmin controllers (cross-company by design). Co-Authored-By: Claude Opus 4.8 --- .../Controllers/BillsController.cs | 2 +- .../Controllers/KioskController.cs | 4 +++- .../Controllers/ToolsController.cs | 22 +++++++++---------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/PowderCoating.Web/Controllers/BillsController.cs b/src/PowderCoating.Web/Controllers/BillsController.cs index 57970fe..a8930fe 100644 --- a/src/PowderCoating.Web/Controllers/BillsController.cs +++ b/src/PowderCoating.Web/Controllers/BillsController.cs @@ -1198,7 +1198,7 @@ public class BillsController : Controller var companyId = int.TryParse(User.FindFirst("CompanyId")?.Value, out var cid) ? cid : 0; var cutoff = DateTime.Today.AddMonths(-12); - var bills = (await _unitOfWork.Bills.GetAllAsync(false, b => b.Vendor)) + var bills = (await _unitOfWork.Bills.FindAsync(b => b.CompanyId == companyId, false, b => b.Vendor)) .Where(b => b.Status != BillStatus.Voided && b.BillDate >= cutoff) .ToList(); diff --git a/src/PowderCoating.Web/Controllers/KioskController.cs b/src/PowderCoating.Web/Controllers/KioskController.cs index f899f77..55e49cd 100644 --- a/src/PowderCoating.Web/Controllers/KioskController.cs +++ b/src/PowderCoating.Web/Controllers/KioskController.cs @@ -582,7 +582,9 @@ public class KioskController : Controller [Authorize] public async Task Intakes(string? filter) { - var sessions = await _unitOfWork.KioskSessions.GetAllAsync(false, + var companyId = GetCurrentCompanyId(); + var sessions = await _unitOfWork.KioskSessions.FindAsync( + s => s.CompanyId == companyId, false, s => s.LinkedCustomer, s => s.LinkedJob); diff --git a/src/PowderCoating.Web/Controllers/ToolsController.cs b/src/PowderCoating.Web/Controllers/ToolsController.cs index d4e16c2..8f6efbd 100644 --- a/src/PowderCoating.Web/Controllers/ToolsController.cs +++ b/src/PowderCoating.Web/Controllers/ToolsController.cs @@ -2267,7 +2267,7 @@ public class ToolsController : Controller } // 15. Purchase Orders - var purchaseOrders = await _unitOfWork.PurchaseOrders.GetAllAsync(false, po => po.Vendor); + var purchaseOrders = await _unitOfWork.PurchaseOrders.FindAsync(po => po.CompanyId == companyId, false, po => po.Vendor); var purchaseOrdersCsv = GeneratePurchaseOrdersCsv(purchaseOrders); var purchaseOrdersEntry = archive.CreateEntry($"purchase_orders_{timestamp}.csv"); using (var entryStream = purchaseOrdersEntry.Open()) @@ -2330,7 +2330,7 @@ public class ToolsController : Controller return RedirectToAction(nameof(Index)); } - var customers = await _unitOfWork.Customers.GetAllAsync(false, c => c.PricingTier); + var customers = await _unitOfWork.Customers.FindAsync(c => c.CompanyId == companyId, false, c => c.PricingTier); var csv = GenerateCustomersCsv(customers); var fileName = $"customers_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; @@ -2364,7 +2364,7 @@ public class ToolsController : Controller return RedirectToAction(nameof(Index)); } - var quotes = await _unitOfWork.Quotes.GetAllAsync(false, q => q.Customer, q => q.QuoteStatus); + var quotes = await _unitOfWork.Quotes.FindAsync(q => q.CompanyId == companyId, false, q => q.Customer, q => q.QuoteStatus); var csv = GenerateQuotesCsv(quotes); var fileName = $"quotes_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; @@ -2397,7 +2397,7 @@ public class ToolsController : Controller return RedirectToAction(nameof(Index)); } - var jobs = await _unitOfWork.Jobs.GetAllAsync(false, j => j.Customer, j => j.JobStatus, j => j.JobPriority); + var jobs = await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId, false, j => j.Customer, j => j.JobStatus, j => j.JobPriority); var csv = GenerateJobsCsv(jobs); var fileName = $"jobs_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; @@ -2429,7 +2429,7 @@ public class ToolsController : Controller return RedirectToAction(nameof(Index)); } - var appointments = await _unitOfWork.Appointments.GetAllAsync(false, + var appointments = await _unitOfWork.Appointments.FindAsync(a => a.CompanyId == companyId, false, a => a.Customer, a => a.AppointmentType, a => a.AppointmentStatus); var csv = GenerateAppointmentsCsv(appointments); var fileName = $"appointments_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; @@ -2499,7 +2499,7 @@ public class ToolsController : Controller return RedirectToAction(nameof(Index)); } - var inventoryItems = await _unitOfWork.InventoryItems.GetAllAsync(false, i => i.PrimaryVendor); + var inventoryItems = await _unitOfWork.InventoryItems.FindAsync(i => i.CompanyId == companyId, false, i => i.PrimaryVendor); var csv = GenerateInventoryCsv(inventoryItems); var fileName = $"inventory_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; @@ -2566,7 +2566,7 @@ public class ToolsController : Controller return RedirectToAction(nameof(Index)); } - var maintenance = await _unitOfWork.MaintenanceRecords.GetAllAsync(false, m => m.Equipment); + var maintenance = await _unitOfWork.MaintenanceRecords.FindAsync(m => m.CompanyId == companyId, false, m => m.Equipment); var csv = GenerateMaintenanceCsv(maintenance); var fileName = $"maintenance_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; @@ -2654,7 +2654,7 @@ public class ToolsController : Controller return RedirectToAction(nameof(Index)); } - var invoices = await _unitOfWork.Invoices.GetAllAsync(false, i => i.Customer, i => i.Job); + var invoices = await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Customer, i => i.Job); var csv = GenerateInvoicesCsv(invoices); var fileName = $"invoices_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; @@ -2687,7 +2687,7 @@ public class ToolsController : Controller return RedirectToAction(nameof(Index)); } - var payments = await _unitOfWork.Payments.GetAllAsync(false, p => p.Invoice, p => p.DepositAccount); + var payments = await _unitOfWork.Payments.FindAsync(p => p.CompanyId == companyId, false, p => p.Invoice, p => p.DepositAccount); var csv = GeneratePaymentsCsv(payments); var fileName = $"payments_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; @@ -2877,7 +2877,7 @@ public class ToolsController : Controller return RedirectToAction(nameof(Index)); } - var purchaseOrders = await _unitOfWork.PurchaseOrders.GetAllAsync(false, po => po.Vendor); + var purchaseOrders = await _unitOfWork.PurchaseOrders.FindAsync(po => po.CompanyId == companyId, false, po => po.Vendor); var csv = GeneratePurchaseOrdersCsv(purchaseOrders); var fileName = $"purchase_orders_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; @@ -4611,7 +4611,7 @@ public class ToolsController : Controller return RedirectToAction(nameof(Index)); } - var expenses = await _unitOfWork.Expenses.GetAllAsync(false, + var expenses = await _unitOfWork.Expenses.FindAsync(e => e.CompanyId == companyId, false, e => e.ExpenseAccount, e => e.PaymentAccount, e => e.Vendor,