Scope all controller account lookups by CompanyId (defense-in-depth sweep)

Completes the read-path defense-in-depth pass flagged in the accounting audit:
every Accounts lookup in a controller now carries an explicit CompanyId predicate,
matching the standing rule in CLAUDE.md ("every FindAsync/GetAllAsync must include
an explicit CompanyId"). ~19 lookups across 12 controllers:

  - Tier 1 (write-path): AccountsController duplicate account-number check (Create/Edit)
  - Tier 2 (dropdowns/lists): Accounts (Index/year-end/parent), BankReconciliations,
    Bills (bank list + receipt scan + suggest), Budgets, CatalogItems, Expenses,
    FixedAssets, Inventory, JournalEntries chart dropdown, Vendors
  - Tier 3 (accountIds.Contains display maps): JournalEntries/Reports/VendorCredits
    detail views, scoped via the in-scope entity's CompanyId for uniformity

companyId source per controller: _tenantContext where available, else the in-scope
entity's CompanyId, else the current user. Build clean; 291 unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 20:49:16 -04:00
parent df194bd64b
commit 08a5cd39d4
13 changed files with 47 additions and 30 deletions
@@ -55,7 +55,8 @@ public class AccountsController : Controller
// GET: /Accounts
public async Task<IActionResult> Index()
{
var accounts = await _unitOfWork.Accounts.GetAllAsync(false, a => a.ParentAccount);
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
var accounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId, false, a => a.ParentAccount);
var dtos = _mapper.Map<List<AccountListDto>>(accounts.OrderBy(a => a.AccountNumber).ToList());
@@ -134,7 +135,7 @@ public class AccountsController : Controller
var currentUser = await _userManager.GetUserAsync(User);
// Check for duplicate account number
var existing = await _unitOfWork.Accounts.FindAsync(a => a.AccountNumber == dto.AccountNumber);
var existing = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == currentUser!.CompanyId && a.AccountNumber == dto.AccountNumber);
if (existing.Any())
{
ModelState.AddModelError(nameof(dto.AccountNumber), "An account with this number already exists.");
@@ -213,7 +214,7 @@ public class AccountsController : Controller
// Check duplicate number (excluding self)
var existing = await _unitOfWork.Accounts.FindAsync(
a => a.AccountNumber == dto.AccountNumber && a.Id != id);
a => a.CompanyId == account.CompanyId && a.AccountNumber == dto.AccountNumber && a.Id != id);
if (existing.Any())
{
ModelState.AddModelError(nameof(dto.AccountNumber), "An account with this number already exists.");
@@ -472,7 +473,7 @@ public class AccountsController : Controller
}
// Load all active accounts with balances
var accounts = (await _unitOfWork.Accounts.FindAsync(a => a.IsActive)).ToList();
var accounts = (await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId && a.IsActive)).ToList();
var revenueAccounts = accounts.Where(a => a.AccountType == AccountType.Revenue).ToList();
var expenseAccounts = accounts.Where(a =>
@@ -616,7 +617,8 @@ public class AccountsController : Controller
/// </summary>
private async Task PopulateDropdownsAsync(int? excludeId = null)
{
var allAccounts = await _unitOfWork.Accounts.FindAsync(a => excludeId == null || a.Id != excludeId.Value);
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
var allAccounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId && (excludeId == null || a.Id != excludeId.Value));
ViewBag.ParentAccounts = allAccounts
.OrderBy(a => a.AccountNumber)