Skip to Content

← All archived runs

Run: fix-tenant-guard-populate

run.md

Run: fix-tenant-guard-populate

  • lane: bug
  • branch: fix-tenant-guard-populate
  • pr: #747

lane/output/notes.md

Bug: fix-tenant-guard-populate

  • observed: /finances (and any page populating a user-discriminator ref) throws [tenant] expert.find called without tenantId and renders an error · expected: the page loads; the populate is a tenant-safe _id join
  • cause: the populate allowlist added with the tenant guard in #739 permits only _id, $or and $and in the filter. Populating a discriminator ref makes Mongoose add the discriminator key (role on the user model) to that filter, so the check fails and the guard throws.
  • fix: packages/services/src/db/plugins/tenant.ts: allow the queried model's discriminator key alongside _id / $or / $and in the populate shape check.
  • changelog: not user-visible as a feature — a regression fix; no entry added

Reproduction

Not reproducible in the agent sandbox (no database, no preview credentials). Reproduced instead from production telemetry plus a deterministic code trace:

  • Vercel runtime errors, project web: group Error: [tenant] expert.find called without tenantId …, routes /finances.rsc, /finances.
  • quoteService.findAll (packages/services/src/db/services/quote/index.ts) populates { path: "expert", … }, plus customer, vendor and manager.
  • getExpertModel (packages/services/src/db/models/user.ts) is a discriminator on the user model, declared with { discriminatorKey: "role" }. Mongoose therefore issues the populate as expert.find({ _id: { $in: [...] }, role: "expert", $or: [ …soft-delete… ] }).
  • role is not in the allowlist → the idOnly test fails → the guard throws.

The error names expert.find — the discriminator model, not the base user model — which is the confirming detail.

Why this is safe

The guard's existing rationale is that ObjectIds are globally unique, so an _id-only join cannot widen results across tenants. The discriminator key only narrows the same join, so allowing it cannot widen anything. Every other named field in a filter still throws, and non-find hooks are untouched.

Blast radius

Any populate of a user-discriminator ref without a tenant filter — quotes and invoices lists, and plausibly other pages that populate expert / customer / vendor / csm. Present on main since #739; only surfaced now because these pages had not been exercised on a preview since.