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
@@ -2558,7 +2558,7 @@ public class ReportsController : Controller
// Load account metadata for budget lines
var accountIds = budget.Lines.Select(l => l.AccountId).Distinct().ToList();
var accounts = (await _unitOfWork.Accounts.FindAsync(a => accountIds.Contains(a.Id)))
var accounts = (await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == budget.CompanyId && accountIds.Contains(a.Id)))
.ToDictionary(a => a.Id);
var rows = new List<BudgetVsActualRow>();