Build a case-export workflow for organizations 6 and 9, both of which contain a case numbered 47. Reviewer 29 belongs to organization 6 and can access only assigned cases; reviewer 62 belongs to organization 9. A list endpoint constrains authorized rows before counting, sorting, and pagination. A case-detail endpoint checks the current actor-record relationship for every direct ID request. Cache entries include tenant and permission scope, but cache keys never replace server authorization. An export request carries a server-authored job envelope with requester, tenant, normalized filter, and a short-lived authorization decision. The worker restricts its query, and the status and artifact download each enforce their own check. Revoke reviewer 29 before a pending job runs and follow the documented cancellation policy. A private response must not enter a shared cache.
Project: tenant-safe case export
Build contract
- Test direct detail, mutation, count, cursor, and mixed-ID bulk requests under both tenants and after a reassignment.
- Run alternating cache requests for the same numeric ID in two tenants; verify protected values never cross and measure query cost.
- Forge a tenant identifier in the export payload and verify the worker uses only server-approved scope.
- Test job status, download, range resume, expiry, and revocation from a second account and from a suspended old tab.
Implementation checkpoint
function exportScope(actor, request) {
if (actor.tenantId !== request.tenantId) return null;
return { requesterId: actor.userId, tenantId: actor.tenantId, filter: request.filter };
}
console.log(exportScope({ userId: 29, tenantId: 6 }, { tenantId: 9, filter: "open" }));
// Output: nullCost and boundaries
A scoped lookup can be O(1) with a suitable tenant and record index, while a careless per-item authorization query makes a fifty-row list generate fifty extra reads. Exporting n permitted rows costs O(n) serialization and storage or transfer; a broad worker query may be faster to write but breaks the tenant boundary. More specific cache keys reduce hit rate and create extra variants, yet prevent cross-tenant disclosure. Rechecking permission at the worker and download boundaries adds small work relative to a full export. Measure request latency, unauthorized denials, cache collisions, job size, artifact cleanup lag, and maximum revocation delay.
Failure drill
Change case 47 to case 62 in a direct request and verify that the server denies both read and write without returning the other case title. Mix authorized and unauthorized IDs in a bulk export. Make the API correctly constrain data but leave the cache keyed only by case number; the second tenant must not see the first response. Forge tenantId 9 in the job payload and inspect the exact worker query. Revoke access after queueing but before generation. Request status and bytes from reviewer 62. Send the artifact through a shared cache and inspect whether one account can receive another account’s response.
Acceptance checks
- Each detail, list, mutation, bulk, and search count applies current server-side object scope.
- Tenant and permission scope survive cache, queue, worker, and artifact boundaries.
- Revocation and expiry block status and download under the chosen policy.
- No private body or metadata is served from a public cache.
Common Mistakes
- Checking only that a caller has a reviewer role.
- Putting tenant ID in a cache key while trusting it from client input.
- Assuming an opaque job ID grants download permission.
- Rechecking permission only at the first API request.
Related lessons
Object-Level Authorization for Reads and Writes; Tenant Scope in Cache and Background Work; Permission Revocation and Active Sessions; Private Export Authorization and Artifact Scope; Search Permissions and Private Results.
