Skip to main content

ADR 004: Return 404 for cross-tenant resource misses

Status: Accepted

Context

RiskFlow is multi-tenant: almost every business row is scoped to a Team via teamId. When a caller requests a resource by ID that exists in another tenant's scope, the API must choose between:

  • 403 Forbidden — "You are authenticated but not allowed to access this resource."
  • 404 Not Found — "No resource exists at this identifier for you."

A 403 confirms that the resource exists but the caller lacks permission. For cross-tenant misses, that confirmation is an information leak: an attacker can probe IDs and learn which resources belong to other tenants.

Decision

Default to 404 over 403 when a resource is missing from the caller's tenant scope.

Implementation guidelines:

  1. Repository and service methods accept the actor's scope (teamId, parent MSP relationship, or vendor org link) and include it in the Prisma where clause.
  2. When the scoped query returns no row, throw NotFoundException — not ForbiddenException — even if an unscoped query would have found a row elsewhere.
  3. Reserve 403 for cases where the resource is in scope but the principal lacks a specific permission (RBAC denial on an owned resource).
  4. Apply the same rule to vendor flows: if a vendor has no VendorOrgLink to the org that owns the resource, respond with 404.

Example pattern:

const org = await this.repository.findByIdForParent(orgId, mspTeamId);
if (!org) {
throw new NotFoundException(`Organization ${orgId} not found`);
}

The caller cannot distinguish "org does not exist" from "org exists under another MSP."

Consequences

  • OpenAPI/Swagger responses for :id routes should document 404 for both genuine misses and cross-tenant probes.
  • Unit tests for tenancy-sensitive services should assert NotFoundException, not ForbiddenException, for out-of-scope IDs.
  • Internal admin or platform endpoints that intentionally cross tenants must be named, guarded, and audited — they are explicit exceptions, not the default CRUD path.
  • Support and debugging may require server-side logs to distinguish a true missing row from a scope rejection; never expose that distinction in the HTTP response.

References

  • .cursor/rules/security-multi-tenancy.mdc — Authorization (RBAC)
  • apps/backend/src/modules/organizations/organizations.service.ts
  • apps/backend/src/modules/agreements/agreements.service.ts