Add explicit CompanyId filter to tenant-scoped GetAllAsync calls

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 10:52:45 -04:00
parent 774f916dae
commit 7c0357b4c5
3 changed files with 15 additions and 13 deletions
@@ -1198,7 +1198,7 @@ public class BillsController : Controller
var companyId = int.TryParse(User.FindFirst("CompanyId")?.Value, out var cid) ? cid : 0; var companyId = int.TryParse(User.FindFirst("CompanyId")?.Value, out var cid) ? cid : 0;
var cutoff = DateTime.Today.AddMonths(-12); 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) .Where(b => b.Status != BillStatus.Voided && b.BillDate >= cutoff)
.ToList(); .ToList();
@@ -582,7 +582,9 @@ public class KioskController : Controller
[Authorize] [Authorize]
public async Task<IActionResult> Intakes(string? filter) public async Task<IActionResult> 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.LinkedCustomer,
s => s.LinkedJob); s => s.LinkedJob);
@@ -2267,7 +2267,7 @@ public class ToolsController : Controller
} }
// 15. Purchase Orders // 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 purchaseOrdersCsv = GeneratePurchaseOrdersCsv(purchaseOrders);
var purchaseOrdersEntry = archive.CreateEntry($"purchase_orders_{timestamp}.csv"); var purchaseOrdersEntry = archive.CreateEntry($"purchase_orders_{timestamp}.csv");
using (var entryStream = purchaseOrdersEntry.Open()) using (var entryStream = purchaseOrdersEntry.Open())
@@ -2330,7 +2330,7 @@ public class ToolsController : Controller
return RedirectToAction(nameof(Index)); 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 csv = GenerateCustomersCsv(customers);
var fileName = $"customers_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; var fileName = $"customers_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
@@ -2364,7 +2364,7 @@ public class ToolsController : Controller
return RedirectToAction(nameof(Index)); 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 csv = GenerateQuotesCsv(quotes);
var fileName = $"quotes_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; var fileName = $"quotes_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
@@ -2397,7 +2397,7 @@ public class ToolsController : Controller
return RedirectToAction(nameof(Index)); 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 csv = GenerateJobsCsv(jobs);
var fileName = $"jobs_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; var fileName = $"jobs_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
@@ -2429,7 +2429,7 @@ public class ToolsController : Controller
return RedirectToAction(nameof(Index)); 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); a => a.Customer, a => a.AppointmentType, a => a.AppointmentStatus);
var csv = GenerateAppointmentsCsv(appointments); var csv = GenerateAppointmentsCsv(appointments);
var fileName = $"appointments_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; var fileName = $"appointments_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
@@ -2499,7 +2499,7 @@ public class ToolsController : Controller
return RedirectToAction(nameof(Index)); 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 csv = GenerateInventoryCsv(inventoryItems);
var fileName = $"inventory_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; var fileName = $"inventory_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
@@ -2566,7 +2566,7 @@ public class ToolsController : Controller
return RedirectToAction(nameof(Index)); 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 csv = GenerateMaintenanceCsv(maintenance);
var fileName = $"maintenance_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; var fileName = $"maintenance_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
@@ -2654,7 +2654,7 @@ public class ToolsController : Controller
return RedirectToAction(nameof(Index)); 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 csv = GenerateInvoicesCsv(invoices);
var fileName = $"invoices_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; var fileName = $"invoices_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
@@ -2687,7 +2687,7 @@ public class ToolsController : Controller
return RedirectToAction(nameof(Index)); 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 csv = GeneratePaymentsCsv(payments);
var fileName = $"payments_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; var fileName = $"payments_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
@@ -2877,7 +2877,7 @@ public class ToolsController : Controller
return RedirectToAction(nameof(Index)); 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 csv = GeneratePurchaseOrdersCsv(purchaseOrders);
var fileName = $"purchase_orders_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; var fileName = $"purchase_orders_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
@@ -4611,7 +4611,7 @@ public class ToolsController : Controller
return RedirectToAction(nameof(Index)); 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.ExpenseAccount,
e => e.PaymentAccount, e => e.PaymentAccount,
e => e.Vendor, e => e.Vendor,